Module: Hecks::Codemod
- Defined in:
- lib/hecks/codemod.rb
Overview
SHARED MACHINERY for a codemod that migrates real .bluebook source
once a DSL builder change makes some previously-required declaration
optional/redundant — pulled out of bin/codemod_implicit_append_fields
(the first one built), which needed three real, hard-won fixes before
it could be trusted: a process-lifetime AST cache with no
invalidation, a batch-revert granularity that let one unsafe
candidate sink every other safe one sharing its boot, and (in the
SPINE the codemod migrates FOR, not here) an append-at-end insertion
that only round-tripped correctly for whichever field happened to be
last. None of those are guessable in advance; they only surface by
actually running a real edit against real self-hosted code. This
module is that lesson, kept — the NEXT codemod plugs in two rule-
specific procs (find_candidates, apply_candidate) and inherits
the boot/safety-net machinery rather than rediscovering it.
A CODEMOD IS NOT PATTERN-MATCHING ALONE. Deciding "is this line safe to delete" means knowing what the RUNTIME would resolve it to — so every codemod built on this module follows the same three steps:
1. Boot the real domain (or the self-hosted meta-domain) and read
its IR to find CANDIDATES — provided by the caller's own
`find_candidates`, since the actual redundancy rule is specific
to whichever spine change this migration serves.
2. Locate and remove each candidate's own source text — the
caller's own `apply_candidate`.
3. Re-boot from the edited text and diff the full IR export
against the pre-edit export. Byte-identical -> keep. Anything
else (including a RAISED exception — the self-hosted
meta-domain DISPATCHES itself into being, S14, so a bad edit
can surface as a runtime refusal, not just a differing export)
-> revert and report SKIPPED, never silently guessed past.
DESIGN-TIME CHECKLIST for the SPINE change a future codemod migrates corpus text for — both items below are real bugs THIS module's own first use found, not hypothetical:
- Does the resolved value get inserted into an ORDER-SENSITIVE
list (the exported IR is array-order-sensitive throughout)? If
so, the spine's own insertion must preserve the ORIGINAL
position, not just append — an append-at-end insertion only
round-trips correctly for a value that already happened to be
last.
- Does resolution depend on ANOTHER construct already being
declared (the aggregate/entity a `sets`/`append:` field resolves
against)? If a creator command can be declared BEFORE the
construct it creates (real, live: `command "Handler"` before
`entity "Handler"`, one file down), one-pass resolution
genuinely cannot see it yet — not a codemod bug, a structural
limit worth naming rather than working around.
Defined Under Namespace
Classes: Runner
Constant Summary collapse
- ROOT =
File.("../..", __dir__)
- EXAMPLE_ROOTS =
Dir.glob(File.join(ROOT, "examples", "*")).select { |p| File.directory?(p) }.sort
- META_FILES =
(Dir.glob(File.join(ROOT, "lib/hecks/grammar/*.bluebook")) + Dir.glob(File.join(ROOT, "lib/hecks/framework/bluebook/*.bluebook")) + Dir.glob(File.join(ROOT, "lib/hecks/language/bluebook/**/*.bluebook"))).sort
- PERSISTENCE_PORT =
THE SAME LIGHTWEIGHT PATH
spec/spec_helper.rb's ownboot_in_memoryuses —Hecks.with_registrysatisfiesHecks.bluebook's owncollect's "loaded outside a boot" check withoutHecks.boot's full era-check/adapter-wiring path, which needs a LIVE Postgres connection forcomplianceand would make every codemod depend on a database it has no reason to touch — a codemod only ever reads a chapter's own declared IR, never a stored record. File.join(ROOT, "lib/hecks/ports/persistence.port")
- EXTRACTION_PORT =
File.join(ROOT, "lib/hecks/ports/extraction.port")
- MEMORY_ADAPTER =
File.join(ROOT, "lib/hecks/adapters/driven/memory.adapter")
- PRISM_ADAPTER =
File.join(ROOT, "lib/hecks/adapters/driven/prism.adapter")
Class Method Summary collapse
-
.boot_meta ⇒ Object
forget_all, not a singleforget— the meta-domain is NINE files (MetaValidator::GRAMMAR_FILES) merged into one registry, and a caller here (the codemod runner) doesn't generally know in advance which ONE it just edited. -
.each_command(registry) ⇒ Object
Walks every aggregate (and every nested entity, recursively) across every chapter in a booted registry, yielding [owning construct, command] pairs —
constructis whichever Aggregate/Entity actually OWNS the command, the same distinctionAggregateBuilder#commandvsEntityBuilder#commandalready draws. -
.element_construct_for(construct, list_field) ⇒ Object
A LIST attribute's own element construct — the value object or entity
list_of(...)names, resolved byhecks_namethe same wayAttributeCollector#resolve_identity_field!already does. - .export_json(registry) ⇒ Object
-
.load_bluebook(path) ⇒ Object
Hecks::Adapters::Prismcaches a file's parsed AST for the life of the PROCESS, keyed by path — fine for every existing caller (a file loads once per process: onebin/irrun, one rspec worker), but a codemod legitimately reloads the SAME path after editing it, and a stale cached tree reports agiven/ensuresblock at its OLD line number, which no longer matches the freshly re-executed file's ownblock.source_location— surfacing as "did not survive extraction" on a perfectly valid file. - .meta_registry ⇒ Object
- .owner_attribute(construct, name) ⇒ Object
-
.safely ⇒ Object
Either a raised exception OR a differing export counts as unsafe — see the module header on why the meta-domain specifically can raise.
Class Method Details
.boot_meta ⇒ Object
forget_all, not a single forget — the meta-domain is NINE
files (MetaValidator::GRAMMAR_FILES) merged into one registry,
and a caller here (the codemod runner) doesn't generally know in
advance which ONE it just edited.
111 112 113 114 115 |
# File 'lib/hecks/codemod.rb', line 111 def self. Hecks::Adapters::Prism.forget_all Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil) export_json(Hecks::Bluebook::MetaValidator.grammar_registry) end |
.each_command(registry) ⇒ Object
Walks every aggregate (and every nested entity, recursively)
across every chapter in a booted registry, yielding [owning
construct, command] pairs — construct is whichever
Aggregate/Entity actually OWNS the command, the same distinction
AggregateBuilder#command vs EntityBuilder#command already
draws. Generic enough for any rule that needs to walk real
commands, not specific to the attribute-redundancy rule.
130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/hecks/codemod.rb', line 130 def self.each_command(registry) registry.bluebooks.each_value do |chapter| chapter.aggregates.each do |aggregate| walk = lambda do |construct| construct.commands.each { |command| yield construct, command } construct.entities.each(&walk) if construct.respond_to?(:entities) end walk.call(aggregate) end end end |
.element_construct_for(construct, list_field) ⇒ Object
A LIST attribute's own element construct — the value object or
entity list_of(...) names, resolved by hecks_name the same way
AttributeCollector#resolve_identity_field! already does. Shared
because "what does this list actually hold" is a question any
append-shaped rule needs answered, not just this one.
151 152 153 154 155 156 157 158 |
# File 'lib/hecks/codemod.rb', line 151 def self.element_construct_for(construct, list_field) list_attr = owner_attribute(construct, list_field) return nil unless list_attr&.list? pool = construct.respond_to?(:value_objects) ? construct.value_objects.dup : [] pool.concat(construct.entities) if construct.respond_to?(:entities) pool.find { |c| c.hecks_name.to_s == list_attr.type.to_s } end |
.export_json(registry) ⇒ Object
72 |
# File 'lib/hecks/codemod.rb', line 72 def self.export_json(registry) = Hecks::Projector::Exporter.json(registry) |
.load_bluebook(path) ⇒ Object
Hecks::Adapters::Prism caches a file's parsed AST for the
life of the PROCESS, keyed by path — fine for every existing
caller (a file loads once per process: one bin/ir run, one
rspec worker), but a codemod legitimately reloads the SAME path
after editing it, and a stale cached tree reports a
given/ensures block at its OLD line number, which no longer
matches the freshly re-executed file's own block.source_location
— surfacing as "did not survive extraction" on a perfectly valid
file. Prism.forget is the real invalidation API this module's
own first use motivated (found here, fixed at the source rather
than left as a private TREES.clear poke from outside).
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/hecks/codemod.rb', line 85 def self.load_bluebook(path) paths = if path.is_a?(Array) path elsif File.directory?(path) Dir.glob(File.join(path, "*.bluebook")) else [path] end paths.each { |file| Hecks::Adapters::Prism.forget(file) } registry = Hecks::Runtime::Registry.new Hecks.with_registry(registry) do Kernel.load(PERSISTENCE_PORT) Kernel.load(EXTRACTION_PORT) Kernel.load(MEMORY_ADAPTER) Kernel.load(PRISM_ADAPTER) Hecks::Bluebook::MetaValidator.defer { paths.each { |file| Kernel.load(file) } } Hecks::Bluebook::MetaValidator.judge_deferred!(registry) end registry end |
.meta_registry ⇒ Object
117 118 119 120 121 |
# File 'lib/hecks/codemod.rb', line 117 def self. Hecks::Adapters::Prism.forget_all Hecks::Bluebook::MetaValidator.instance_variable_set(:@grammar_registry, nil) Hecks::Bluebook::MetaValidator.grammar_registry end |
.owner_attribute(construct, name) ⇒ Object
142 143 144 |
# File 'lib/hecks/codemod.rb', line 142 def self.owner_attribute(construct, name) construct.attributes.find { |attr| attr.name.to_s == name.to_s } end |
.safely ⇒ Object
Either a raised exception OR a differing export counts as unsafe — see the module header on why the meta-domain specifically can raise. Returns [value_or_nil, error_message_or_nil].
163 164 165 166 167 |
# File 'lib/hecks/codemod.rb', line 163 def self.safely [yield, nil] rescue StandardError => e [nil, "#{e.class}: #{e.}"] end |