hecks 2.9.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/hecks +51 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +16 -3
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +13 -4
- data/lib/hecks/adapters/driven/prism.rb +41 -8
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
- data/lib/hecks/behaviors/expectations.rb +1 -1
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
- data/lib/hecks/bluebook/chapter.rb +6 -2
- data/lib/hecks/bluebook/domain_port.rb +36 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
- data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
- data/lib/hecks/bluebook/expression/resolver.rb +3 -2
- data/lib/hecks/bluebook/hexagon.rb +13 -5
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator.rb +9 -4
- data/lib/hecks/bluebook/model_check.rb +23 -0
- data/lib/hecks/bluebook/pattern_subset.rb +38 -0
- data/lib/hecks/bluebook/query.rb +20 -5
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +2 -2
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +1 -1
- data/lib/hecks/cli/mcp.rb +3 -3
- data/lib/hecks/cli/model_check.rb +15 -8
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +224 -32
- data/lib/hecks/cli/project_diagrams.rb +4 -3
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +3 -3
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +5 -3
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -5
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +201 -52
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +1 -1
- data/lib/hecks/{facade → doors}/handle.rb +2 -2
- data/lib/hecks/{facade → doors}/json_door.rb +4 -4
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
- data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
- data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +1 -1
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +24 -37
- data/lib/hecks/forms/app.rb +6 -6
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/field_shape.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +1 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
- data/lib/hecks/language/bluebook/query.bluebook +7 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
- data/lib/hecks/mcp_stdio_guard.rb +2 -2
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +15 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/state_codec.rb +24 -5
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +5 -5
- data/lib/hecks/projections/deploy/lambda.rb +43 -32
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/template.rb +1 -1
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/shared.rb +1 -1
- data/lib/hecks/projections/diagrams.rb +3 -3
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/parser_table.rb +4 -4
- data/lib/hecks/projections/reference.rb +2 -3
- data/lib/hecks/projections/rust_vocabulary.rb +1 -1
- data/lib/hecks/projector/cli_projector.rb +106 -37
- data/lib/hecks/projector.rb +1 -1
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/release/runner/ci_publisher.rb +2 -2
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +14 -7
- data/lib/hecks/release/runner/npm_publisher.rb +6 -36
- data/lib/hecks/release/runner/preflight.rb +26 -2
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +30 -15
- data/lib/hecks/release/runner.rb +57 -6
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
- data/lib/hecks/runtime/dependency_planning.rb +20 -2
- data/lib/hecks/runtime/dispatcher.rb +53 -3
- data/lib/hecks/runtime/interpreting.rb +3 -0
- data/lib/hecks/runtime/loader.rb +65 -12
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/policy_interpreter.rb +5 -2
- data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
- data/lib/hecks/runtime/query_interpreter.rb +77 -0
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/refusal_wording.rb +23 -1
- data/lib/hecks/runtime/registry/verification.rb +163 -4
- data/lib/hecks/runtime/registry.rb +57 -3
- data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
- data/lib/hecks/runtime/value/coercion.rb +23 -135
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +4 -1
- data/lib/hecks/runtime.rb +20 -6
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +16 -16
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +32 -15
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +477 -12
- data/lib/hecks/facade/cli_door.rb +0 -96
- data/lib/hecks/facade/cli_runner.rb +0 -127
- data/lib/hecks/mcp_door_scope.rb +0 -95
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require "json"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "ripper"
|
|
8
|
+
require_relative "../tools"
|
|
9
|
+
|
|
10
|
+
module Hecks
|
|
11
|
+
module Tools
|
|
12
|
+
# Checks a set of Ruby files against docs/COMMENT_STYLE_GUIDE.md and,
|
|
13
|
+
# for the mechanical categories, rewrites them to comply.
|
|
14
|
+
module CommentStyle
|
|
15
|
+
MAX_LINE = 100
|
|
16
|
+
LONG_CLASS_DOC = 25
|
|
17
|
+
MAX_HEADING_WORDS = 6
|
|
18
|
+
|
|
19
|
+
# A run of comment lines longer than this is a `long_block`, unless the baseline holds it.
|
|
20
|
+
# ADR 0075 ticket 01 sets this value, amending ADR 0069's provisional 50.
|
|
21
|
+
MAX_BLOCK = 12
|
|
22
|
+
|
|
23
|
+
# Longest anchor kept in a block's baseline key.
|
|
24
|
+
MAX_ANCHOR = 80
|
|
25
|
+
|
|
26
|
+
CATEGORIES = {
|
|
27
|
+
"undocumented_method" => "public method has no doc comment",
|
|
28
|
+
"missing_param" => "doc comment lacks @param for a parameter",
|
|
29
|
+
"missing_return" => "doc comment lacks @return",
|
|
30
|
+
"missing_raise" => "method raises but doc comment lacks @raise",
|
|
31
|
+
"undocumented_class" => "class or module has no doc comment",
|
|
32
|
+
"unstructured_class_doc" => "long class doc has no ## section headers",
|
|
33
|
+
"missing_summary" => "doc comment has tags but no lead sentence",
|
|
34
|
+
"all_caps" => "all-caps emphasis",
|
|
35
|
+
"long_bold" => "bold heading over #{MAX_HEADING_WORDS} words reads as shouting",
|
|
36
|
+
"bare_constant" => "capitalised word names something in this file's code: backtick or lowercase it",
|
|
37
|
+
"unknown_capitals" => "capitalised token is not an English word: backtick it or list it as an acronym",
|
|
38
|
+
"design_history" => "narrates design history instead of citing an ADR",
|
|
39
|
+
"long_line" => "comment line exceeds #{MAX_LINE} characters",
|
|
40
|
+
"long_block" => "comment block exceeds #{MAX_BLOCK} lines and is new or has grown past its baseline"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
FIXABLE = %w[all_caps long_bold long_line].freeze
|
|
44
|
+
|
|
45
|
+
# Initialisms and keywords that are correctly written in capitals.
|
|
46
|
+
ACRONYMS = %w[
|
|
47
|
+
ARGV ARN AZ ECR ES FSM GVL IAM IGW IPC IRB LLM MB MVCC NAT OOM RDS SG SIGKILL SIGTERM STDIO TOML
|
|
48
|
+
UI VCS VM VPC WASI
|
|
49
|
+
ABC ACID ADR API ASC ASCII AST AWS BFS BYPASSRLS CAS CDN CEO CI CLI CPU CRUD CSRF CSS CSV CTE DB DBA DDD DDL
|
|
50
|
+
DELETE DESC DFS DML DNS DSL ENV ERB ERD FIFO FIXME FK FQN GC GET GIN HMAC HTML HTTP HTTPS ID INSERT IO IR ISO
|
|
51
|
+
JS JSON JSONB JSONL JWKS JWT LIFO LRU MCP MDN MRI MVP NAMEDATALEN NIL NULL OIDC OK ORM OS
|
|
52
|
+
PATCH PG PID PK PM POSIX POST PR PRAGMA PRD PUT QA QC RAM RBAC README REPL REST RLS RNG RPC RUBYOPT
|
|
53
|
+
SAM SAVEPOINT SDK SELECT SHA SME SQL SSH SSL SSM SW STDERR STDIN STDOUT TCP TLA TLS TOCTOU TODO TTL UID UL
|
|
54
|
+
URI URL UTC UTF UUID VO WASM XML YAML
|
|
55
|
+
ACL ALB BC GNU OAC WAF
|
|
56
|
+
CLOUDFRONT ECS HSM KMS
|
|
57
|
+
].to_set.freeze
|
|
58
|
+
|
|
59
|
+
# Names whose correct spelling is neither all-caps nor all-lowercase.
|
|
60
|
+
PROPER_NOUNS = {
|
|
61
|
+
"CLOUDFLARE" => "Cloudflare", "OAUTH" => "OAuth", "RSPEC" => "RSpec", "JAVASCRIPT" => "JavaScript",
|
|
62
|
+
"GITHUB" => "GitHub", "GOOGLE" => "Google", "GRAPHVIZ" => "Graphviz", "HECKS" => "Hecks",
|
|
63
|
+
"POSTGRES" => "Postgres", "POSTGRESQL" => "PostgreSQL", "PUMA" => "Puma", "RAILS" => "Rails",
|
|
64
|
+
"RUBY" => "Ruby", "RUST" => "Rust", "SQLITE" => "SQLite"
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# Multi-word SQL is protected as a phrase, because the individual keywords
|
|
68
|
+
# (`FROM`, `WHERE`, `TABLE`) are also ordinary English emphasis targets.
|
|
69
|
+
SQL_PHRASES = Regexp.union(
|
|
70
|
+
/\bINSERT INTO\b/, /\bSELECT\b.*?\bFROM\b/, /\bORDER BY\b/, /\bGROUP BY\b/,
|
|
71
|
+
/\bON CONFLICT\b(?: DO (?:NOTHING|UPDATE))?/, /\bDO NOTHING\b/, /\bFOR (?:UPDATE|SHARE)\b/,
|
|
72
|
+
/\bSKIP LOCKED\b/, /\b(?:PRIMARY|FOREIGN) KEY\b/, /\bIS(?: NOT)? NULL\b/, /\bNOT NULL\b/,
|
|
73
|
+
/\bCREATE (?:UNIQUE )?(?:TABLE|INDEX|POLICY|ROLE|SCHEMA|TRIGGER|FUNCTION)\b/,
|
|
74
|
+
/\bALTER (?:TABLE|ROLE|SCHEMA)\b/, /\bDROP (?:TABLE|COLUMN|INDEX|SCHEMA|POLICY)\b/,
|
|
75
|
+
/\bADD (?:COLUMN|CONSTRAINT)\b/, /\bDELETE FROM\b/, /\bUNION ALL\b/, /\bSET LOCAL\b/,
|
|
76
|
+
/\bBEGIN (?:IMMEDIATE|EXCLUSIVE|DEFERRED)\b/, /\bROW LEVEL SECURITY\b/,
|
|
77
|
+
/\bCREATE \.\.\. PARTITION OF\b/, /\bPARTITION OF\b/, /\bIF (?:NOT )?EXISTS\b/,
|
|
78
|
+
/\b(?:LEFT|INNER|OUTER|CROSS) JOIN\b/, /\bLIMIT \d+\b/, /\bOWNER TO\b/, /\b(?:on|in|onto) PATH\b/
|
|
79
|
+
).freeze
|
|
80
|
+
|
|
81
|
+
# SQL keywords that are rarely English emphasis. Kept in capitals on a line
|
|
82
|
+
# that reads as SQL, and always kept in a file that talks to a database.
|
|
83
|
+
SQL_STRONG = %w[
|
|
84
|
+
ALTER ATTACH BEGIN CASCADE COMMIT CONCURRENTLY DETACH DISTINCT FORCE GRANT LIMIT MATERIALIZED
|
|
85
|
+
NULLS OFFSET PARTITION RETURNING REVOKE ROLLBACK SERIALIZABLE VACUUM
|
|
86
|
+
].to_set.freeze
|
|
87
|
+
|
|
88
|
+
# SQL keywords that are also everyday emphasis targets (`FROM`, `FIRST`, `SET`).
|
|
89
|
+
SQL_WEAK = %w[
|
|
90
|
+
COLUMN CONSTRAINT CREATE DEFAULT DROP EXCLUSIVE EXISTS FIRST FROM INDEX JOIN LAST LOCK LOGIN
|
|
91
|
+
NOBYPASSRLS NOCREATEDB NOCREATEROLE NOLOGIN NOSUPERUSER OWNER POLICY
|
|
92
|
+
REFERENCES ROLE SCHEMA SET SHARE TABLE TRANSACTION TRIGGER UNION UNIQUE UPDATE VALUES VIEW WHERE
|
|
93
|
+
].to_set.freeze
|
|
94
|
+
|
|
95
|
+
SQL_NEUTRAL = %w[ALL AND AS BY FOR IN IS NOT OF ON OR TO].to_set.freeze
|
|
96
|
+
|
|
97
|
+
SQL_PATH = %r{sql|postgres|/d1|_era|/era|lineage|outbox|schema|persistence}
|
|
98
|
+
|
|
99
|
+
PROTECTED_SPANS = Regexp.union(
|
|
100
|
+
/`[^`]*`/, /"[^"]*"/, /'[^'\s]*'/, %r{\b[a-z]+://\S+}, SQL_PHRASES,
|
|
101
|
+
/\b(?:NOTE|OPTIMIZE|HACK|REVIEW):/
|
|
102
|
+
).freeze
|
|
103
|
+
|
|
104
|
+
CAPS_WORD = /
|
|
105
|
+
(?<![\w:.\#@$'])
|
|
106
|
+
(?:A|[A-Z]{2,}(?:'[A-Z]{1,2})?)
|
|
107
|
+
(?![\w(]|'(?!s\b|\s|$)|-[a-z](?<=POST-.)|::|\.\w|\#\d|\s*=(?![=~>])|\s*\u2190)
|
|
108
|
+
/x
|
|
109
|
+
|
|
110
|
+
HISTORY = [
|
|
111
|
+
/\bused to\b/i, /\bwas originally\b/i, /\boriginally (?:was|were|used|lived|named|called)\b/i,
|
|
112
|
+
/\bpreviously\b/i, /\bformerly\b/i, /\bhistorically\b/i, /\bstale as of\b/i,
|
|
113
|
+
/\b(?:was|were|been|got) renamed\b/i, /\brenamed from\b/i, /\bhas since\b/i,
|
|
114
|
+
/\bbefore (?:this|the) (?:change|fix|refactor|split|rename|pr|commit|patch)\b/i, /\bBefore this, /,
|
|
115
|
+
/\ban? earlier (?:version|revision|draft|pass)\b/i, /\bwhen this was (?:written|added)\b/i,
|
|
116
|
+
/\b(?:in|since|by|after|before|until) (?:PR )?\#\d{2,}/i, /\bPR \#?\d{2,}\b/,
|
|
117
|
+
/\bno longer\b/i, /\bany more\b/i, /\banymore\b/i
|
|
118
|
+
].freeze
|
|
119
|
+
|
|
120
|
+
DIRECTIVE = /\A#\s*(?:rubocop:|frozen_string_literal:|encoding:|coding:|shareable_constant|typed:|!)/
|
|
121
|
+
|
|
122
|
+
SELF_EVIDENT = %w[
|
|
123
|
+
to_s to_str to_sym to_h to_a to_proc inspect hash eql? == === <=> method_missing
|
|
124
|
+
respond_to_missing? pretty_print
|
|
125
|
+
].to_set.freeze
|
|
126
|
+
|
|
127
|
+
# Stands in for a protected span so it still reads as a word, not a gap.
|
|
128
|
+
FILLER = "\u00A7"
|
|
129
|
+
|
|
130
|
+
# What may sit between the words of one all-caps heading run.
|
|
131
|
+
GAP = %r{\A(?:[\s,/&'\-\d]|#{FILLER}|[A-Z]{2,})*\z}
|
|
132
|
+
|
|
133
|
+
# A bold phrase opening a comment line, after an optional bullet.
|
|
134
|
+
BOLD_HEADING = /\A(#+\s*(?:[-*]\s+)?)\*\*([^*]+)\*\*/
|
|
135
|
+
|
|
136
|
+
# What ends a heading run: an em dash, or sentence punctuation.
|
|
137
|
+
HEADING_END = /\A(?:\s+—|[.:!?](?:\s|\z))/
|
|
138
|
+
|
|
139
|
+
CODE_NOISE = %i[on_comment on_sp on_nl on_ignored_nl].freeze
|
|
140
|
+
|
|
141
|
+
Violation = Struct.new(:path, :line, :category, :message)
|
|
142
|
+
MethodDef = Struct.new(:name, :line, :params, :visibility, :raises, :block_param, :type_fqn)
|
|
143
|
+
TypeDef = Struct.new(:fqn, :line, :namespace_only)
|
|
144
|
+
Comment = Struct.new(:line, :col, :text, :full_line)
|
|
145
|
+
Block = Struct.new(:line, :lines, :key)
|
|
146
|
+
|
|
147
|
+
# Decides whether a capitalised token is an English word being shouted;
|
|
148
|
+
# anything else keeps its capitals and is reported for a person to look at.
|
|
149
|
+
module Dictionary
|
|
150
|
+
WORDS = "/usr/share/dict/words"
|
|
151
|
+
|
|
152
|
+
# Technical vocabulary the system word list lacks.
|
|
153
|
+
EXTRA = %w[
|
|
154
|
+
adversarial allowlist anymore app apps arg args arity async backend backtrace baseline
|
|
155
|
+
boolean bootable bootstrapping bluebook byte callee canonicalized changelog checklist
|
|
156
|
+
checkpoint codebase codemod config database dataset dedupe deduped enqueue evaluator
|
|
157
|
+
filename fixpoint formatting frontend glob grep handover hardcoded hecksagon heredoc held
|
|
158
|
+
hoc hostname idempotency idempotent inline keyword landmine lifecycle lookup mapping
|
|
159
|
+
memoised memoized memoizes metadata metaprogrammed monkeypatch monorepo multi mutex
|
|
160
|
+
namespace noop numeric op ops parameterized params pathname payload plugin postcondition
|
|
161
|
+
pre reachability reentrant refactor regex regexes rekey rekeyed repo restartable retrofit
|
|
162
|
+
reusable roadmap rollup runtime shortcut standalone stringified struct subprocess
|
|
163
|
+
subprocesses subroutine substring superuser symlink sync timestamp tmp tmpl touchpoint
|
|
164
|
+
trickiest triggerable tuple undecoded unsanitized untargeted upsert username webhook
|
|
165
|
+
widget workaround worklist worklists
|
|
166
|
+
].freeze
|
|
167
|
+
|
|
168
|
+
SUFFIXES = [
|
|
169
|
+
["", ""], ["s", ""], ["es", ""], ["ed", ""], ["d", ""], ["ing", ""], ["ing", "e"], ["ies", "y"],
|
|
170
|
+
["ied", "y"], ["ly", ""], ["er", ""], ["est", ""], ["ier", "y"], ["ised", "ized"], ["ise", "ize"],
|
|
171
|
+
["isation", "ization"], ["our", "or"], ["lling", "ling"], ["lled", "led"]
|
|
172
|
+
].freeze
|
|
173
|
+
|
|
174
|
+
module_function
|
|
175
|
+
|
|
176
|
+
# Loads and memoizes the system word list, once per process.
|
|
177
|
+
def words
|
|
178
|
+
return @words if defined?(@words)
|
|
179
|
+
|
|
180
|
+
@words = File.exist?(WORDS) ? Set.new(File.foreach(WORDS, chomp: true).map(&:downcase)).merge(EXTRA) : nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Checks a capitalized token against the word list, stripped and stemmed.
|
|
184
|
+
# Always true when this machine has no word list to consult.
|
|
185
|
+
def english?(word)
|
|
186
|
+
return true unless words
|
|
187
|
+
|
|
188
|
+
bare = word.downcase.sub(/n't\z/, "").sub(/'.*\z/, "")
|
|
189
|
+
SUFFIXES.any? do |suffix, replacement|
|
|
190
|
+
next false unless bare.end_with?(suffix)
|
|
191
|
+
|
|
192
|
+
stem = bare.delete_suffix(suffix) + replacement
|
|
193
|
+
words.include?(stem) || words.include?(stem.sub(/(.)\1\z/, '\1'))
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Class and module names defined under `lib/`, keyed by their shouted form.
|
|
199
|
+
def self.class_names
|
|
200
|
+
@class_names ||= Dir["lib/**/*.rb"].each_with_object({}) do |file, names|
|
|
201
|
+
File.read(file).scan(/^\s*(?:class|module)\s+([A-Z]\w*)/) do |(name)|
|
|
202
|
+
names[name.upcase] ||= name if name.match?(/[a-z]/)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Lexes `source` and drops every comment and whitespace token.
|
|
208
|
+
def self.code_tokens(source)
|
|
209
|
+
Ripper.lex(source).reject { |token| CODE_NOISE.include?(token[1]) }.map { |token| token[2] }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Bare class/module names on Hecks's documented public surface (ADR 0075 ticket 01):
|
|
213
|
+
# an operator or a client calls these directly. The DSL itself (`aggregate`,
|
|
214
|
+
# `command`, `query`, ...) is invoked through the `Hecks.bluebook` block, not by
|
|
215
|
+
# naming a builder class, so its builder classes stay off this list.
|
|
216
|
+
#
|
|
217
|
+
# A method's structural checks (tags, `undocumented_method`) apply only when its
|
|
218
|
+
# innermost enclosing type's own name is here; everything else is internal and
|
|
219
|
+
# needs no doc unless it carries a real why.
|
|
220
|
+
PUBLIC_SURFACE = Set["Storehouse"].freeze
|
|
221
|
+
|
|
222
|
+
def self.public_surface
|
|
223
|
+
PUBLIC_SURFACE
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# The checked-in list of comment blocks already over `MAX_BLOCK` lines.
|
|
227
|
+
#
|
|
228
|
+
# Keyed by first-line text, not line number, so edits above a block don't break its key.
|
|
229
|
+
module Baseline
|
|
230
|
+
FILE = ".standardize_comments_baseline.json"
|
|
231
|
+
|
|
232
|
+
# The checkout this file lives in: the baseline's home when no `root:` is given.
|
|
233
|
+
ROOT = File.expand_path("../../..", __dir__)
|
|
234
|
+
|
|
235
|
+
PATH = File.join(ROOT, FILE)
|
|
236
|
+
|
|
237
|
+
module_function
|
|
238
|
+
|
|
239
|
+
# The baseline's location inside a checkout.
|
|
240
|
+
#
|
|
241
|
+
# @param root [String] the checkout's root directory
|
|
242
|
+
# @return [String] the baseline file's path
|
|
243
|
+
def path_in(root) = File.join(root, FILE)
|
|
244
|
+
|
|
245
|
+
# Reads the baseline from disk.
|
|
246
|
+
def load(path = PATH)
|
|
247
|
+
File.exist?(path) ? JSON.parse(File.read(path, encoding: Encoding::UTF_8)) : {}
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Writes a baseline in a stable order, so a rewrite shows only real changes in a diff.
|
|
251
|
+
def dump(blocks, path = PATH)
|
|
252
|
+
sorted = blocks.sort.to_h { |file, keys| [file, keys.sort.to_h] }
|
|
253
|
+
File.write(path, "#{JSON.pretty_generate(sorted)}\n")
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# One parsed source file and the violations found in it.
|
|
258
|
+
class SourceFile
|
|
259
|
+
attr_reader :path, :lines, :comments
|
|
260
|
+
|
|
261
|
+
def initialize(path, source = nil)
|
|
262
|
+
@path = path
|
|
263
|
+
@source = source || File.read(path)
|
|
264
|
+
@lines = @source.lines
|
|
265
|
+
@comments = lex_comments
|
|
266
|
+
@by_line = @comments.to_h { |c| [c.line, c] }
|
|
267
|
+
@open_spans = open_spans
|
|
268
|
+
@constants = code_words
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Finds every place this file departs from the style guide.
|
|
272
|
+
def violations(only: nil, baseline: {})
|
|
273
|
+
return [] if generated?
|
|
274
|
+
|
|
275
|
+
found = text_violations + (test_file? ? [] : structure_violations) + long_block_violations(baseline)
|
|
276
|
+
found = found.select { |v| only.include?(v.category) } if only
|
|
277
|
+
found.sort_by { |v| [v.line, v.category] }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Finds every run of consecutive full-line comments longer than `MAX_BLOCK`.
|
|
281
|
+
def long_blocks
|
|
282
|
+
return [] if generated?
|
|
283
|
+
|
|
284
|
+
seen = Hash.new(0)
|
|
285
|
+
runs = @comments.select(&:full_line).slice_when { |above, below| below.line != above.line + 1 }
|
|
286
|
+
runs.select { |run| run.size > MAX_BLOCK }.map { |run| Block.new(run.first.line, run.size, block_key(run, seen)) }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Rewrites the mechanical categories; raises rather than write a change that alters code.
|
|
290
|
+
def fixed(only: nil)
|
|
291
|
+
return @source if generated?
|
|
292
|
+
|
|
293
|
+
wanted = (only || FIXABLE) & FIXABLE
|
|
294
|
+
output = @lines.map.with_index(1) { |line, number| fix_line(line, number, wanted) }.join
|
|
295
|
+
return @source if output == @source
|
|
296
|
+
|
|
297
|
+
unchanged = CommentStyle.code_tokens(@source) == CommentStyle.code_tokens(output)
|
|
298
|
+
raise "#{path}: fix would change code, refusing" unless unchanged
|
|
299
|
+
|
|
300
|
+
output
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Parses this file's structure once and caches it.
|
|
304
|
+
def types
|
|
305
|
+
test_file? || generated? ? [] : structure.last
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# A generated file's comments belong to its generator, and its
|
|
309
|
+
# `GENERATED ... DO NOT EDIT` banner is a convention tools recognise.
|
|
310
|
+
def generated?
|
|
311
|
+
@lines.first(8).join.match?(/\bgenerated\b.*?\bdo not edit\b/im)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Helpers defined inside a spec are test scaffolding, not API, so the
|
|
315
|
+
# doc-tag categories skip them. The prose categories still apply.
|
|
316
|
+
def test_file?
|
|
317
|
+
path.end_with?("_spec.rb")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# A `class`/`module` is documented only if a comment block sits directly above it.
|
|
321
|
+
def documented?(type)
|
|
322
|
+
!doc_above(type.line).empty?
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
# The text of a block's first line, without its `#`, is its key. A second block opened by
|
|
328
|
+
# the same text in this file gets a `#2` suffix, and so on.
|
|
329
|
+
def block_key(run, seen)
|
|
330
|
+
text = run.first.text.sub(/\A#+\s*/, "").strip[0, MAX_ANCHOR]
|
|
331
|
+
text = "(empty)" if text.empty?
|
|
332
|
+
seen[text] += 1
|
|
333
|
+
seen[text] == 1 ? text : "#{text} ##{seen[text]}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# A block fails when it has no baseline entry, or has grown past the length recorded there.
|
|
337
|
+
def long_block_violations(baseline)
|
|
338
|
+
held = baseline.fetch(path.delete_prefix("./"), {})
|
|
339
|
+
long_blocks.filter_map do |block|
|
|
340
|
+
allowed = held[block.key]
|
|
341
|
+
next if allowed && block.lines <= allowed
|
|
342
|
+
|
|
343
|
+
detail = allowed ? "baselined at #{allowed}" : "over #{MAX_BLOCK}"
|
|
344
|
+
Violation.new(path, block.line, "long_block", "#{block.lines} lines, #{detail}: #{block.key}")
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def lex_comments
|
|
349
|
+
Ripper.lex(@source).filter_map do |(line, col), type, text, _|
|
|
350
|
+
next unless type == :on_comment
|
|
351
|
+
|
|
352
|
+
prefix = @lines[line - 1].byteslice(0, col)
|
|
353
|
+
Comment.new(line, col, text.chomp, prefix.strip.empty?)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def text_violations
|
|
358
|
+
@comments.flat_map do |comment|
|
|
359
|
+
next [] if comment.text.match?(DIRECTIVE)
|
|
360
|
+
|
|
361
|
+
caps_violations(comment) + constant_violations(comment) + unknown_violations(comment) +
|
|
362
|
+
bold_violations(comment) + history_violations(comment) + length_violations(comment)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# All-caps names in this file's code: constants, heredoc delimiters, and
|
|
367
|
+
# strings that are nothing but one such name (`ENV["PATH"]`).
|
|
368
|
+
def code_words
|
|
369
|
+
Ripper.lex(@source).each_with_object(Set.new) do |(_, type, text, _), words|
|
|
370
|
+
case type
|
|
371
|
+
when :on_const, :on_tstring_content then words << text if text.match?(/\A[A-Z][A-Z0-9_]+\z/)
|
|
372
|
+
when :on_heredoc_beg, :on_heredoc_end then words.merge(text.scan(/[A-Z][A-Z0-9_]+/))
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def unknown_violations(comment)
|
|
378
|
+
words = mask(comment).scan(CAPS_WORD).uniq - ["A"]
|
|
379
|
+
words.reject { |word| keep_capitals?(word) }.select { |word| unknown?(word) }.map do |word|
|
|
380
|
+
Violation.new(path, comment.line, "unknown_capitals", word)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def bold_violations(comment)
|
|
385
|
+
heading = comment.text[BOLD_HEADING, 2]
|
|
386
|
+
return [] unless heading && heading.split.size > MAX_HEADING_WORDS
|
|
387
|
+
|
|
388
|
+
[Violation.new(path, comment.line, "long_bold", "#{heading.split.size} words")]
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def caps_violations(comment)
|
|
392
|
+
caps_words(comment).map do |_, word, replacement|
|
|
393
|
+
Violation.new(path, comment.line, "all_caps", "#{word} -> #{replacement}")
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# A capitalised word that is also a constant defined in this file could be
|
|
398
|
+
# either prose or a reference, so it is reported rather than rewritten.
|
|
399
|
+
def constant_violations(comment)
|
|
400
|
+
words = mask(comment).scan(CAPS_WORD).uniq
|
|
401
|
+
words.select { |word| @constants.include?(word) && !ACRONYMS.include?(word) }.map do |word|
|
|
402
|
+
Violation.new(path, comment.line, "bare_constant", word)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def history_violations(comment)
|
|
407
|
+
masked = comment.text.gsub(/`[^`]*`/) { |span| " " * span.length }
|
|
408
|
+
pattern = HISTORY.find { |candidate| masked.match?(candidate) }
|
|
409
|
+
return [] unless pattern
|
|
410
|
+
|
|
411
|
+
[Violation.new(path, comment.line, "design_history", %("#{masked[pattern]}"))]
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def length_violations(comment)
|
|
415
|
+
length = @lines[comment.line - 1].chomp.length
|
|
416
|
+
return [] if length <= MAX_LINE
|
|
417
|
+
|
|
418
|
+
[Violation.new(path, comment.line, "long_line", "#{length} characters")]
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def caps_words(comment)
|
|
422
|
+
masked = mask(comment)
|
|
423
|
+
matches = []
|
|
424
|
+
masked.scan(CAPS_WORD) { matches << Regexp.last_match }
|
|
425
|
+
sql = sql_keywords(comment, matches.map { |match| match[0] })
|
|
426
|
+
found = matches.filter_map do |match|
|
|
427
|
+
word = match[0]
|
|
428
|
+
next if word != "A" && (keep_capitals?(word) || sql.include?(word) || unknown?(word))
|
|
429
|
+
|
|
430
|
+
[match.begin(0), word, recase(word, sentence_start?(comment, masked, match.begin(0)))]
|
|
431
|
+
end
|
|
432
|
+
(drop_stray_articles(found) + possessives(masked)).sort_by(&:first)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# The comment with every span that must keep its capitals blanked out. A
|
|
436
|
+
# backtick span can wrap across lines, so whether this line opens inside
|
|
437
|
+
# one comes from the lines above it.
|
|
438
|
+
def mask(comment)
|
|
439
|
+
text = comment.text
|
|
440
|
+
head = ""
|
|
441
|
+
if @open_spans[comment.line]
|
|
442
|
+
lead = text[/\A#+\s*/].length
|
|
443
|
+
close = text.index("`", lead)
|
|
444
|
+
cut = close ? close + 1 : text.length
|
|
445
|
+
head = text[0, lead] + (FILLER * (cut - lead))
|
|
446
|
+
text = text[cut..]
|
|
447
|
+
end
|
|
448
|
+
body = text.gsub(PROTECTED_SPANS) { |span| quoted_heading?(span) ? span : FILLER * span.length }
|
|
449
|
+
open = body.rindex("`")
|
|
450
|
+
body = body[0, open] + (FILLER * (body.length - open)) if open
|
|
451
|
+
head + body
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Which lines begin inside a backtick span opened earlier in the paragraph.
|
|
455
|
+
def open_spans
|
|
456
|
+
inside = false
|
|
457
|
+
@comments.each_with_object({}) do |comment, open|
|
|
458
|
+
above = @by_line[comment.line - 1]
|
|
459
|
+
continues = comment.full_line && above&.full_line && !above.text.match?(/\A#+\s*\z/)
|
|
460
|
+
inside = false unless continues
|
|
461
|
+
open[comment.line] = inside
|
|
462
|
+
inside ^= comment.text.count("`").odd?
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Capitalised SQL keywords to leave alone on this line. A keyword such as
|
|
467
|
+
# `FROM` or `SET` is only SQL when the rest of the line reads as SQL too.
|
|
468
|
+
def sql_keywords(comment, words)
|
|
469
|
+
bare = words.map { |word| word.sub(/'[A-Z]{1,2}\z/, "") } - ["A"]
|
|
470
|
+
strong = bare.select { |word| SQL_STRONG.include?(word) }
|
|
471
|
+
weak = bare.select { |word| SQL_WEAK.include?(word) }
|
|
472
|
+
database_file = path.match?(SQL_PATH)
|
|
473
|
+
return bare.to_set if reads_as_sql?(bare, strong, weak, database_file)
|
|
474
|
+
return (strong + weak).to_set if comment.text.match?(SQL_PHRASES)
|
|
475
|
+
|
|
476
|
+
database_file ? strong.to_set : Set.new
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def reads_as_sql?(bare, strong, weak, database_file)
|
|
480
|
+
only_sql = bare.all? do |word|
|
|
481
|
+
[SQL_STRONG, SQL_WEAK, SQL_NEUTRAL, ACRONYMS].any? { |set| set.include?(word) }
|
|
482
|
+
end
|
|
483
|
+
return false unless only_sql
|
|
484
|
+
|
|
485
|
+
database_file ? (strong.any? || weak.any?) : (strong.any? && (strong + weak).size >= 2)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# `Finding`'s: the capital follows a code span, so `CAPS_WORD` never sees it.
|
|
489
|
+
def possessives(masked)
|
|
490
|
+
found = []
|
|
491
|
+
masked.scan(/(?<=#{FILLER})'S\b/) { found << [Regexp.last_match.begin(0) + 1, "S", "s"] }
|
|
492
|
+
found
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# An "A" is only emphasis when a word beside it is being fixed too.
|
|
496
|
+
def drop_stray_articles(found)
|
|
497
|
+
found.each_with_index.reject do |(offset, word, _), index|
|
|
498
|
+
before = index.positive? ? found[index - 1] : nil
|
|
499
|
+
after = found[index + 1]
|
|
500
|
+
beside = (before && before[0] + before[1].length + 1 == offset && before[1] != "A") ||
|
|
501
|
+
(after && after[0] == offset + 2 && after[1] != "A")
|
|
502
|
+
word == "A" && !beside
|
|
503
|
+
end.map(&:first)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# A quoted run of capitals is another comment's heading, not a literal.
|
|
507
|
+
def quoted_heading?(span)
|
|
508
|
+
return false unless span.start_with?('"') && !span.match?(SQL_PHRASES)
|
|
509
|
+
|
|
510
|
+
capitals = span.scan(/\b[A-Z]{2,}\b/).size
|
|
511
|
+
capitals >= 3 || (capitals == 2 && !span.match?(/[a-z]/))
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def recase(word, sentence_start)
|
|
515
|
+
bare, suffix = word.split("'", 2)
|
|
516
|
+
proper = proper_name(bare)
|
|
517
|
+
return [proper, suffix&.downcase].compact.join("'") if proper
|
|
518
|
+
|
|
519
|
+
sentence_start ? word.capitalize : word.downcase
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# A shouted product or class name (`POSTGRESERA`) gets its real spelling
|
|
523
|
+
# back. An English word that is also a class name (`VALUE`) stays prose.
|
|
524
|
+
def proper_name(bare)
|
|
525
|
+
PROPER_NOUNS[bare] || (CommentStyle.class_names[bare] unless Dictionary.english?(bare))
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Neither English nor a known name, so lowercasing it would be a guess.
|
|
529
|
+
def unknown?(word)
|
|
530
|
+
bare = word.split("'", 2).first
|
|
531
|
+
!Dictionary.english?(word) && !proper_name(bare)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def keep_capitals?(word)
|
|
535
|
+
bare = word.sub(/'[A-Z]{1,2}\z/, "")
|
|
536
|
+
return false if PROPER_NOUNS.key?(bare)
|
|
537
|
+
|
|
538
|
+
ACRONYMS.include?(bare) || @constants.include?(bare) || bare.match?(/\A[IVX]+\z/)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def sentence_start?(comment, masked, offset)
|
|
542
|
+
before = masked[0, offset].sub(/\A#+\s*/, "")
|
|
543
|
+
return before.match?(/[.!?]["')\]]*\s+\z/) if before.match?(/[[:alnum:]#{FILLER}]/)
|
|
544
|
+
return true if before.match?(/\A\s*(?:[-*•]|─+|\d+[.)])\s+\z/)
|
|
545
|
+
return false if masked[0, offset].match?(/\A#\s{3,}/)
|
|
546
|
+
|
|
547
|
+
previous = comment.full_line ? @by_line[comment.line - 1] : nil
|
|
548
|
+
return true unless previous&.full_line
|
|
549
|
+
|
|
550
|
+
body = previous.text.sub(/\A#+\s*/, "").strip
|
|
551
|
+
body.empty? || body.match?(/[.!?]["')\]]*\z/)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def fix_line(line, number, wanted)
|
|
555
|
+
comment = @by_line[number]
|
|
556
|
+
return line if comment.nil? || comment.text.match?(DIRECTIVE)
|
|
557
|
+
|
|
558
|
+
text = wanted.include?("all_caps") ? fix_caps(comment) : comment.text
|
|
559
|
+
text = unbold(text) if wanted.include?("long_bold")
|
|
560
|
+
code = line.byteslice(0, comment.col)
|
|
561
|
+
return wrap(code, text) if wanted.include?("long_line") && comment.full_line
|
|
562
|
+
|
|
563
|
+
"#{code}#{text}#{"\n" if line.end_with?("\n")}"
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def unbold(text)
|
|
567
|
+
heading = text[BOLD_HEADING, 2]
|
|
568
|
+
return text unless heading && heading.split.size > MAX_HEADING_WORDS
|
|
569
|
+
|
|
570
|
+
text.sub(BOLD_HEADING) { "#{Regexp.last_match(1)}#{heading}" }
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def fix_caps(comment)
|
|
574
|
+
text = comment.text.dup
|
|
575
|
+
found = caps_words(comment)
|
|
576
|
+
found.reverse_each { |offset, word, replacement| text[offset, word.length] = replacement }
|
|
577
|
+
first, last = heading_run(comment, found)
|
|
578
|
+
return text unless first
|
|
579
|
+
|
|
580
|
+
text.insert(last, "**").insert(first, "**")
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# An all-caps run that opens a paragraph and stops at a dash or a full stop
|
|
584
|
+
# is that paragraph's heading, so it becomes bold rather than plain text.
|
|
585
|
+
def heading_run(comment, found)
|
|
586
|
+
masked = mask(comment)
|
|
587
|
+
return unless found.size >= 2 && paragraph_start?(comment, masked, found.first[0])
|
|
588
|
+
|
|
589
|
+
run = found.slice_when { |(a, word, _), (b, _, _)| !masked[(a + word.length)...b].match?(GAP) }.first
|
|
590
|
+
finish = run.last[0] + run.last[1].length
|
|
591
|
+
finish += 2 if masked[finish, 2] == "'s"
|
|
592
|
+
return unless run.size >= 2 && masked[finish..].match?(HEADING_END)
|
|
593
|
+
return if masked[run.first[0]...finish].split.size > MAX_HEADING_WORDS || documents_method?(comment)
|
|
594
|
+
|
|
595
|
+
[run.first[0], finish]
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# The first line above a `def` has to be a sentence about the method, so a
|
|
599
|
+
# heading there stays plain text.
|
|
600
|
+
def documents_method?(comment)
|
|
601
|
+
line = comment.line
|
|
602
|
+
line += 1 while @by_line[line + 1]&.full_line
|
|
603
|
+
@lines[line].to_s.match?(/\A\s*(?:\w+\s+)?def\s/)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def paragraph_start?(comment, masked, offset)
|
|
607
|
+
return false if masked[0, offset].sub(/\A#+\s*/, "").match?(/[[:alnum:]#{FILLER}]/)
|
|
608
|
+
|
|
609
|
+
above = comment.full_line ? @by_line[comment.line - 1] : nil
|
|
610
|
+
!above&.full_line || above.text.match?(/\A#+\s*\z/)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Splits one over-long prose comment line in two. Lines with no safe
|
|
614
|
+
# break point (a long URL, an unbroken code span) are left for a person.
|
|
615
|
+
def wrap(indent, text)
|
|
616
|
+
whole = "#{indent}#{text}\n"
|
|
617
|
+
return whole if whole.chomp.length <= MAX_LINE
|
|
618
|
+
|
|
619
|
+
lead = text[/\A#+\s*/]
|
|
620
|
+
lead += " " if text.match?(/\A#+\s*@\w/)
|
|
621
|
+
cut = text[0, MAX_LINE - indent.length + 1].rindex(" ")
|
|
622
|
+
return whole if cut.nil? || cut <= lead.length || text[0, cut].count("`").odd?
|
|
623
|
+
|
|
624
|
+
"#{indent}#{text[0, cut].rstrip}\n#{wrap(indent, "#{lead}#{text[cut..].strip}")}"
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def structure_violations
|
|
628
|
+
methods, types = structure
|
|
629
|
+
methods.flat_map { |m| method_violations(m) } + types.flat_map { |t| long_doc_violations(t) }
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def structure
|
|
633
|
+
@structure ||= begin
|
|
634
|
+
walker = Walker.new
|
|
635
|
+
sexp = Ripper.sexp(@source)
|
|
636
|
+
walker.walk(sexp, Walker::Scope.new([], :public)) if sexp
|
|
637
|
+
[walker.methods, walker.types]
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def doc_above(line)
|
|
642
|
+
block = []
|
|
643
|
+
cursor = line - 1
|
|
644
|
+
while (comment = @by_line[cursor]) && comment.full_line
|
|
645
|
+
block.unshift(comment.text)
|
|
646
|
+
cursor -= 1
|
|
647
|
+
end
|
|
648
|
+
block
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def method_violations(method)
|
|
652
|
+
return [] unless method.visibility == :public
|
|
653
|
+
return [] unless CommentStyle.public_surface.include?(method.type_fqn.to_s.split("::").last)
|
|
654
|
+
|
|
655
|
+
doc = doc_above(method.line).join("\n")
|
|
656
|
+
if doc.empty?
|
|
657
|
+
return [] if SELF_EVIDENT.include?(method.name)
|
|
658
|
+
|
|
659
|
+
return [Violation.new(path, method.line, "undocumented_method", "##{method.name}")]
|
|
660
|
+
end
|
|
661
|
+
return [] if doc.match?(/@api private|@private|:nodoc:|\(see (?:`[^`]+`|#\w)[^)]*\)\W*\z/m)
|
|
662
|
+
|
|
663
|
+
tag_violations(method, doc)
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def tag_violations(method, doc)
|
|
667
|
+
param_violations(method, doc) + summary_violations(method, doc) + outcome_violations(method, doc)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def param_violations(method, doc)
|
|
671
|
+
found = method.params.reject { |name| doc.match?(/@(?:param|option)\s+(?:\[[^\]\n]*\]\s*)?#{Regexp.escape(name)}\b/) }
|
|
672
|
+
.map { |name| violation(method, "missing_param", "lacks @param #{name}") }
|
|
673
|
+
block = method.block_param
|
|
674
|
+
if block && !doc.match?(/@yield|@param\s+(?:\[[^\]\n]*\]\s*)?#{Regexp.escape(block)}\b/)
|
|
675
|
+
found << violation(method, "missing_param", "lacks @yield for &#{block}")
|
|
676
|
+
end
|
|
677
|
+
found
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def summary_violations(method, doc)
|
|
681
|
+
found = []
|
|
682
|
+
found << violation(method, "missing_summary", "") if tags_only?(doc) && method.name != "initialize"
|
|
683
|
+
if heading_lead?(doc)
|
|
684
|
+
found << violation(method, "missing_summary",
|
|
685
|
+
"opens with a bold heading: add a verb-first lead sentence above it")
|
|
686
|
+
end
|
|
687
|
+
found
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def outcome_violations(method, doc)
|
|
691
|
+
found = []
|
|
692
|
+
returns = method.name == "initialize" || method.name.end_with?("=") || doc.include?("@return")
|
|
693
|
+
found << violation(method, "missing_return", "") unless returns
|
|
694
|
+
found << violation(method, "missing_raise", "") if method.raises && !doc.include?("@raise")
|
|
695
|
+
found
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def tags_only?(doc)
|
|
699
|
+
lead_line(doc).start_with?("@")
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# A bold heading is a paragraph title, not a sentence about the method.
|
|
703
|
+
def heading_lead?(doc)
|
|
704
|
+
lead_line(doc).start_with?("**")
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def lead_line(doc)
|
|
708
|
+
doc.lines.map { |line| line.sub(/\A\s*#+\s?/, "").strip }.reject(&:empty?).first.to_s
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def violation(method, category, detail)
|
|
712
|
+
Violation.new(path, method.line, category, "##{method.name} #{detail}".strip)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def long_doc_violations(type)
|
|
716
|
+
doc = doc_above(type.line)
|
|
717
|
+
return [] if doc.length <= LONG_CLASS_DOC || doc.any? { |l| l.match?(/\A#\s*##+ /) }
|
|
718
|
+
|
|
719
|
+
[Violation.new(path, type.line, "unstructured_class_doc", "#{type.fqn} (#{doc.length} lines)")]
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
# Walks a Ripper s-expression collecting method and type definitions
|
|
724
|
+
# along with the visibility each method ends up with.
|
|
725
|
+
class Walker
|
|
726
|
+
Scope = Struct.new(:nesting, :visibility)
|
|
727
|
+
|
|
728
|
+
attr_reader :methods, :types
|
|
729
|
+
|
|
730
|
+
def initialize
|
|
731
|
+
@methods = []
|
|
732
|
+
@types = []
|
|
733
|
+
@private_names = Set.new
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# Visits one Ripper node, recording any method or type it defines.
|
|
737
|
+
def walk(node, scope)
|
|
738
|
+
return unless node.is_a?(Array)
|
|
739
|
+
|
|
740
|
+
case node[0]
|
|
741
|
+
when :class then open_type(node[1], node[3], scope)
|
|
742
|
+
when :module then open_type(node[1], node[2], scope)
|
|
743
|
+
when :sclass then walk_body(node[2], Scope.new(scope.nesting, :public))
|
|
744
|
+
when :def then record(node[1], node[2], node[3], scope.visibility, scope.nesting)
|
|
745
|
+
when :defs then record(node[3], node[4], node[5], :public, scope.nesting)
|
|
746
|
+
when :vcall, :var_ref then switch_visibility(node, scope)
|
|
747
|
+
when :command, :method_add_arg then visibility_call?(node, scope) || walk_children(node, scope)
|
|
748
|
+
else walk_children(node, scope)
|
|
749
|
+
end
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
private
|
|
753
|
+
|
|
754
|
+
def walk_children(node, scope)
|
|
755
|
+
node.each { |child| walk(child, scope) if child.is_a?(Array) }
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# `private :name` can follow the definition, so it is applied once the
|
|
759
|
+
# whole body has been seen.
|
|
760
|
+
def walk_body(body, scope)
|
|
761
|
+
first = @methods.length
|
|
762
|
+
outer = @private_names
|
|
763
|
+
@private_names = Set.new
|
|
764
|
+
walk_children(body, scope) if body.is_a?(Array)
|
|
765
|
+
@methods[first..].each { |m| m.visibility = :private if @private_names.include?(m.name) }
|
|
766
|
+
@private_names = outer
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def open_type(const, body, scope)
|
|
770
|
+
nesting = scope.nesting + [const_name(const)]
|
|
771
|
+
statements = body.is_a?(Array) && body[0] == :bodystmt ? Array(body[1]) : []
|
|
772
|
+
namespace = statements.all? { |s| !s.is_a?(Array) || %i[class module void_stmt].include?(s[0]) }
|
|
773
|
+
@types << TypeDef.new(nesting.join("::"), line_of(const), namespace)
|
|
774
|
+
walk_body(body, Scope.new(nesting, :public))
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
def const_name(node)
|
|
778
|
+
node.flatten.grep(String).join("::")
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def line_of(node)
|
|
782
|
+
position = node.flatten.each_cons(2).find { |a, b| a.is_a?(Integer) && b.is_a?(Integer) }
|
|
783
|
+
position ? position[0] : 0
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def switch_visibility(node, scope)
|
|
787
|
+
case node[1].is_a?(Array) ? node[1][1] : nil
|
|
788
|
+
when "private", "protected" then scope.visibility = :private
|
|
789
|
+
when "public", "module_function" then scope.visibility = :public
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def visibility_call?(node, scope)
|
|
794
|
+
head = node[0] == :command ? node[1] : node.dig(1, 1)
|
|
795
|
+
return false unless head.is_a?(Array) && head[0] == :@ident
|
|
796
|
+
return false unless %w[private protected private_class_method].include?(head[1])
|
|
797
|
+
|
|
798
|
+
arguments = node[0] == :command ? node[2] : node.dig(2, 1)
|
|
799
|
+
Array(arguments.is_a?(Array) ? arguments[1] : nil).each do |argument|
|
|
800
|
+
next unless argument.is_a?(Array)
|
|
801
|
+
|
|
802
|
+
case argument[0]
|
|
803
|
+
when :def then record(argument[1], argument[2], argument[3], :private, scope.nesting)
|
|
804
|
+
when :defs then record(argument[3], argument[4], argument[5], :private, scope.nesting)
|
|
805
|
+
when :symbol_literal, :dyna_symbol
|
|
806
|
+
@private_names << argument.flatten.grep(String).last
|
|
807
|
+
else walk(argument, scope)
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
true
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def record(name_node, params, body, visibility, nesting)
|
|
814
|
+
names, block = parameter_names(params)
|
|
815
|
+
@methods << MethodDef.new(name_node[1], name_node[2][0], names, visibility, raises?(body), block,
|
|
816
|
+
nesting.join("::"))
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def parameter_names(params)
|
|
820
|
+
params = params[1] if params.is_a?(Array) && params[0] == :paren
|
|
821
|
+
return [[], nil] unless params.is_a?(Array) && params[0] == :params
|
|
822
|
+
|
|
823
|
+
_, required, optional, rest, post, keywords, keyrest, block = params
|
|
824
|
+
names = idents(required) + Array(optional).map { |pair| pair[0][1] } + splat_name(rest) +
|
|
825
|
+
idents(post) + Array(keywords).map { |pair| pair[0][1].chomp(":") } + splat_name(keyrest)
|
|
826
|
+
[names, splat_name(block).first]
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def idents(list)
|
|
830
|
+
Array(list).select { |item| item.is_a?(Array) && item[0] == :@ident }.map { |item| item[1] }
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
# `rest`/`keyrest`/`block` are each either nil, an unnamed splat (`*`, `**`, `&`), or a
|
|
834
|
+
# one-element `[:@ident, name, pos]` node wrapping the name.
|
|
835
|
+
def splat_name(node)
|
|
836
|
+
node.is_a?(Array) && node[1].is_a?(Array) ? [node[1][1]] : []
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def raises?(node)
|
|
840
|
+
return false unless node.is_a?(Array)
|
|
841
|
+
return false if %i[def defs].include?(node[0])
|
|
842
|
+
|
|
843
|
+
call = %i[command fcall vcall].include?(node[0]) && node[1].is_a?(Array) && node[1][1] == "raise"
|
|
844
|
+
call || node.any? { |child| raises?(child) }
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# Lints a set of paths and renders the result.
|
|
849
|
+
class Run
|
|
850
|
+
# @param paths [Array<String>] files or directories
|
|
851
|
+
# @param only [Array<String>, nil] categories to keep
|
|
852
|
+
# @param baseline [Hash{String => Hash{String => Integer}}] the long blocks to tolerate
|
|
853
|
+
def initialize(paths, only: nil, baseline: Baseline.load)
|
|
854
|
+
@files = paths.flat_map { |p| File.directory?(p) ? ruby_files(p) : [p] }.sort.uniq
|
|
855
|
+
@only = only
|
|
856
|
+
@baseline = baseline
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# Every block over `MAX_BLOCK` lines in these files, in the baseline's own shape.
|
|
860
|
+
#
|
|
861
|
+
# @return [Hash{String => Hash{String => Integer}}] file path to block key to length
|
|
862
|
+
def long_block_baseline
|
|
863
|
+
@files.each_with_object({}) do |path, held|
|
|
864
|
+
blocks = SourceFile.new(path).long_blocks
|
|
865
|
+
held[path.delete_prefix("./")] = blocks.to_h { |block| [block.key, block.lines] } if blocks.any?
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
# Files whose code differs from `ref`, comments and whitespace aside.
|
|
870
|
+
#
|
|
871
|
+
# A file `ref` does not hold (new since `ref`, or outside the repository) counts as
|
|
872
|
+
# changed: what cannot be compared is not vouched for.
|
|
873
|
+
#
|
|
874
|
+
# @param ref [String] revision to compare the working tree against
|
|
875
|
+
# @return [Array<String>] paths whose non-comment token stream changed
|
|
876
|
+
# @raise [ArgumentError] when `ref` names no commit
|
|
877
|
+
def code_changed_since(ref)
|
|
878
|
+
top = vcs_output("rev-parse", "--show-toplevel", chdir: Dir.pwd)&.strip
|
|
879
|
+
raise ArgumentError, "not inside a checkout" if top.nil? || top.empty?
|
|
880
|
+
unless vcs_output("rev-parse", "--verify", "--quiet", "#{ref}^{commit}", chdir: top)
|
|
881
|
+
raise ArgumentError, "unknown ref #{ref.inspect}"
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
@files.select do |path|
|
|
885
|
+
before = committed_source(ref, path, top)
|
|
886
|
+
before.nil? || CommentStyle.code_tokens(before) != CommentStyle.code_tokens(File.read(path))
|
|
887
|
+
end
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# Every violation across the whole file set, computed once.
|
|
891
|
+
#
|
|
892
|
+
# @return [Array<Violation>]
|
|
893
|
+
def violations
|
|
894
|
+
@violations ||= begin
|
|
895
|
+
sources = @files.map { |path| SourceFile.new(path) }
|
|
896
|
+
found = sources.flat_map { |source| source.violations(only: @only, baseline: @baseline) } +
|
|
897
|
+
undocumented_types(sources)
|
|
898
|
+
@only ? found.select { |v| @only.include?(v.category) } : found
|
|
899
|
+
end
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
# Rewrites every file's fixable categories in place.
|
|
903
|
+
#
|
|
904
|
+
# @return [Integer] number of files rewritten
|
|
905
|
+
def fix!
|
|
906
|
+
wants_caps = @only.nil? || @only.include?("all_caps")
|
|
907
|
+
abort "fixing all_caps needs the word list at #{Dictionary::WORDS}" if wants_caps && !Dictionary.words
|
|
908
|
+
|
|
909
|
+
@files.count do |path|
|
|
910
|
+
original = File.read(path)
|
|
911
|
+
output = SourceFile.new(path, original).fixed(only: @only)
|
|
912
|
+
next false if output == original
|
|
913
|
+
|
|
914
|
+
File.write(path, output)
|
|
915
|
+
true
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# Renders the summary tables `--report` prints.
|
|
920
|
+
#
|
|
921
|
+
# @param top [Integer] rows to show in the per-file table
|
|
922
|
+
# @return [String]
|
|
923
|
+
def report(top: 20)
|
|
924
|
+
by_file = violations.group_by(&:path)
|
|
925
|
+
[
|
|
926
|
+
"#{violations.size} violations in #{by_file.size} of #{@files.size} files\n",
|
|
927
|
+
"By category:", *category_rows,
|
|
928
|
+
"\nBy directory (top #{top}):", *directory_rows.first(top),
|
|
929
|
+
"\nWorst #{top} files:", *file_rows(by_file, top)
|
|
930
|
+
].join("\n")
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
private
|
|
934
|
+
|
|
935
|
+
# `path`'s text at `ref`, or nil when the repository at `top` holds none there.
|
|
936
|
+
def committed_source(ref, path, top)
|
|
937
|
+
relative = Pathname.new(File.expand_path(path)).relative_path_from(Pathname.new(top)).to_s
|
|
938
|
+
return nil if relative == ".." || relative.start_with?("../")
|
|
939
|
+
|
|
940
|
+
vcs_output("show", "#{ref}:#{relative}", chdir: top)&.force_encoding(Encoding::UTF_8)
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
# What `git <args>` printed, or nil when it failed.
|
|
944
|
+
def vcs_output(*args, chdir:)
|
|
945
|
+
out = IO.popen(["git", *args], chdir: chdir, err: File::NULL, &:read)
|
|
946
|
+
$CHILD_STATUS.success? ? out : nil
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
# `.rb` files, plus extensionless scripts with a Ruby shebang.
|
|
950
|
+
def ruby_files(dir)
|
|
951
|
+
scripts = Dir[File.join(dir, "**", "*")].select do |path|
|
|
952
|
+
File.file?(path) && File.extname(path).empty? && File.open(path, &:gets).to_s.match?(/\A#!.*ruby/)
|
|
953
|
+
end
|
|
954
|
+
Dir[File.join(dir, "**", "*.rb")] + scripts
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
def category_rows
|
|
958
|
+
counts = violations.group_by(&:category).transform_values(&:size)
|
|
959
|
+
CATEGORIES.map do |name, description|
|
|
960
|
+
suffix = FIXABLE.include?(name) ? " [fixable]" : ""
|
|
961
|
+
" #{counts.fetch(name, 0).to_s.rjust(6)} #{name.ljust(24)} #{description}#{suffix}"
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
def directory_rows
|
|
966
|
+
violations.group_by { |v| v.path.split("/")[0, 3].join("/") }
|
|
967
|
+
.sort_by { |_, list| -list.size }
|
|
968
|
+
.map { |dir, list| " #{list.size.to_s.rjust(6)} #{dir}" }
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
def file_rows(by_file, top)
|
|
972
|
+
by_file.sort_by { |_, list| -list.size }.first(top).map do |file, list|
|
|
973
|
+
category, worst = list.group_by(&:category).max_by { |_, group| group.size }
|
|
974
|
+
" #{list.size.to_s.rjust(6)} #{file} (mostly #{category}: #{worst.size})"
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# A type reopened across files counts as documented if any opening is.
|
|
979
|
+
def undocumented_types(sources)
|
|
980
|
+
openings = sources.flat_map { |s| s.types.reject(&:namespace_only).map { |t| [s, t] } }
|
|
981
|
+
openings.group_by { |_, type| type.fqn }.filter_map do |fqn, group|
|
|
982
|
+
next unless CommentStyle.public_surface.include?(fqn.to_s.split("::").last)
|
|
983
|
+
next if group.any? { |source, type| source.documented?(type) }
|
|
984
|
+
|
|
985
|
+
source, type = group.first
|
|
986
|
+
Violation.new(source.path, type.line, "undocumented_class", fqn)
|
|
987
|
+
end
|
|
988
|
+
end
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
# Builds the CLI's option parser, filling `options` as flags are seen.
|
|
992
|
+
def self.option_parser(options)
|
|
993
|
+
OptionParser.new do |opts|
|
|
994
|
+
opts.banner = "Usage: hecks check_comments [--report|--check|--fix] [--only a,b] PATH..."
|
|
995
|
+
opts.on("--report", "summary tables (default)") { options[:mode] = :report }
|
|
996
|
+
opts.on("--check", "list every violation, exit 1 if any") { options[:mode] = :check }
|
|
997
|
+
opts.on("--fix", "rewrite fixable categories: #{FIXABLE.join(', ')}") { options[:mode] = :fix }
|
|
998
|
+
opts.on("--write-baseline", "record every block over #{MAX_BLOCK} lines as tolerated") do
|
|
999
|
+
options[:mode] = :write_baseline
|
|
1000
|
+
end
|
|
1001
|
+
opts.on("--only LIST", Array, "limit to: #{CATEGORIES.keys.join(', ')}") { |list| options[:only] = list }
|
|
1002
|
+
opts.on("--code-unchanged REF", "exit 1 if any file's code (not comments) differs from REF") do |ref|
|
|
1003
|
+
options[:mode] = :code_unchanged
|
|
1004
|
+
options[:ref] = ref
|
|
1005
|
+
end
|
|
1006
|
+
opts.on("--json", "machine-readable output") { options[:json] = true }
|
|
1007
|
+
opts.on("--top N", Integer, "rows in the worst-files table") { |n| options[:top] = n }
|
|
1008
|
+
end
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
# Prints `--report`, `--check`, or `--json` output for a finished run.
|
|
1012
|
+
def self.render(run, options)
|
|
1013
|
+
found = run.violations
|
|
1014
|
+
if options[:json]
|
|
1015
|
+
puts JSON.pretty_generate(found.map(&:to_h))
|
|
1016
|
+
elsif options[:mode] == :check
|
|
1017
|
+
found.each { |v| puts "#{v.path}:#{v.line}: [#{v.category}] #{v.message}" }
|
|
1018
|
+
else
|
|
1019
|
+
puts run.report(top: options[:top])
|
|
1020
|
+
end
|
|
1021
|
+
options[:mode] == :check && found.any? ? 1 : 0
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
# The CLI entry point: parses `argv` and runs the requested mode.
|
|
1025
|
+
#
|
|
1026
|
+
# Paths after a `--` are never read as flags, and a path that does not exist is refused.
|
|
1027
|
+
def self.main(argv, root: Baseline::ROOT)
|
|
1028
|
+
options = { mode: :report, top: 20 }
|
|
1029
|
+
parser = option_parser(options)
|
|
1030
|
+
paths = parser.parse(argv)
|
|
1031
|
+
unknown = Array(options[:only]) - CATEGORIES.keys
|
|
1032
|
+
abort "unknown categories: #{unknown.join(', ')}" unless unknown.empty?
|
|
1033
|
+
abort parser.help if paths.empty?
|
|
1034
|
+
missing = paths.reject { |path| File.exist?(path) }
|
|
1035
|
+
abort "no such path: #{missing.join(', ')}" unless missing.empty?
|
|
1036
|
+
|
|
1037
|
+
baseline_path = Baseline.path_in(root)
|
|
1038
|
+
run = Run.new(paths, only: options[:only], baseline: Baseline.load(baseline_path))
|
|
1039
|
+
case options[:mode]
|
|
1040
|
+
when :fix then puts("rewrote #{run.fix!} files") || 0
|
|
1041
|
+
when :write_baseline then write_baseline(run, baseline_path)
|
|
1042
|
+
when :code_unchanged then report_code_changes(changed_since(run, options[:ref]))
|
|
1043
|
+
else render(run, options)
|
|
1044
|
+
end
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
# Rewrites the baseline file from the blocks over `MAX_BLOCK` lines that `run` finds.
|
|
1048
|
+
def self.write_baseline(run, path = Baseline::PATH)
|
|
1049
|
+
held = run.long_block_baseline
|
|
1050
|
+
Baseline.dump(held, path)
|
|
1051
|
+
puts "recorded #{held.values.sum(&:size)} blocks in #{held.size} files at #{path}"
|
|
1052
|
+
0
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
# The files `run` finds changed since `ref`, refusing a ref that names no commit.
|
|
1056
|
+
def self.changed_since(run, ref)
|
|
1057
|
+
run.code_changed_since(ref)
|
|
1058
|
+
rescue ArgumentError => e
|
|
1059
|
+
abort e.message
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
# Prints `--code-unchanged`'s verdict.
|
|
1063
|
+
def self.report_code_changes(changed)
|
|
1064
|
+
changed.each { |path| puts "#{path}: code changed, not just comments" }
|
|
1065
|
+
puts "code unchanged" if changed.empty?
|
|
1066
|
+
changed.empty? ? 0 : 1
|
|
1067
|
+
end
|
|
1068
|
+
end
|
|
1069
|
+
end
|
|
1070
|
+
end
|