Class: DynamicLocals::ASTRewriter
- Inherits:
-
Object
- Object
- DynamicLocals::ASTRewriter
- Defined in:
- lib/dynamic_locals/ast_rewriter.rb
Defined Under Namespace
Classes: Replacement
Constant Summary collapse
- WRAPPER_NAME =
The source we translate is a method body: it may legally use constructs that are only valid inside a method --
yield, a top-levelreturn, and an implicit begin/rescue (raise; rescue; ...). Parsing it as a standalone program rejects those, so we parse it wrapped in a method definition and then map node offsets back onto the original (unwrapped) source. "__dynamic_locals_parse_wrapper__"- WRAPPER_PREFIX =
"def #{WRAPPER_NAME}\n"- WRAPPER_SUFFIX =
"\nend"
Instance Attribute Summary collapse
-
#original_src ⇒ Object
readonly
Returns the value of attribute original_src.
Instance Method Summary collapse
-
#ast ⇒ Object
The method body scope: a StatementsNode, a BeginNode (when the body has a rescue/else/ensure), or nil when the body is empty.
-
#initialize(source) ⇒ ASTRewriter
constructor
A new instance of ASTRewriter.
- #insert_after(node, src) ⇒ Object
-
#insert_at(wrapped_offset, src) ⇒ Object
Offsets are relative to the wrapped source, as in Prism locations.
- #insert_before(node, src) ⇒ Object
-
#locals ⇒ Object
The method-level local table Ruby derives from the body.
- #modified_src ⇒ Object
- #replace(node, src) ⇒ Object
- #replace_offsets(wrapped_start, wrapped_end, src) ⇒ Object
Constructor Details
#initialize(source) ⇒ ASTRewriter
Returns a new instance of ASTRewriter.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 14 def initialize(source) @original_src = source.dup.freeze result, prefix = parse_with_locals([]) errors = result.errors.reject { |error| error.type == :no_local_variable } raise SyntaxError, errors.map(&:message).join("\n") if errors.any? # A pin such as `^foo` reports :no_local_variable when `foo` will only be # supplied dynamically. Reparse with those names declared on the wrapper # method; pins in blocks then resolve naturally, while a pin beyond a real # scope boundary (a nested def/class/module body) remains a syntax error. implicit_locals = result.errors.filter_map do |error| error.location.slice.to_sym if error.type == :no_local_variable end.uniq result, prefix = parse_with_locals(implicit_locals) if implicit_locals.any? raise SyntaxError, result.errors.map(&:message).join("\n") unless result.success? @offset = prefix.bytesize @def_node = result.value.statements.body.first @replacements = [] end |
Instance Attribute Details
#original_src ⇒ Object (readonly)
Returns the value of attribute original_src.
38 39 40 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 38 def original_src @original_src end |
Instance Method Details
#ast ⇒ Object
The method body scope: a StatementsNode, a BeginNode (when the body has a rescue/else/ensure), or nil when the body is empty.
42 43 44 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 42 def ast @def_node.body end |
#insert_after(node, src) ⇒ Object
74 75 76 77 78 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 74 def insert_after(node, src) raise TypeError unless Prism::Node === node insert_at(node.location.end_offset, src) end |
#insert_at(wrapped_offset, src) ⇒ Object
Offsets are relative to the wrapped source, as in Prism locations.
81 82 83 84 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 81 def insert_at(wrapped_offset, src) pos = wrapped_offset - @offset @replacements << Replacement.new(pos...pos, src) end |
#insert_before(node, src) ⇒ Object
68 69 70 71 72 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 68 def insert_before(node, src) raise TypeError unless Prism::Node === node insert_at(node.location.start_offset, src) end |
#locals ⇒ Object
The method-level local table Ruby derives from the body.
47 48 49 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 47 def locals @def_node.locals end |
#modified_src ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 51 def modified_src # Apply right-to-left so earlier ranges stay valid. Ties are broken so # that a zero-width insertion at an offset is applied before a # replacement ending there (keeping the insertion outside the replaced # text), and same-position insertions land in registration order. rewrites = @replacements.each_with_index.sort_by do |replacement, index| [replacement.range.end, replacement.range.begin, index] end.reverse src = original_src.b rewrites.each do |replacement, _| src[replacement.range] = replacement.src.b end src.force_encoding(original_src.encoding) end |
#replace(node, src) ⇒ Object
90 91 92 93 94 95 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 90 def replace(node, src) raise TypeError unless Prism::Node === node range = range_from(node) @replacements << Replacement.new(range, src) end |
#replace_offsets(wrapped_start, wrapped_end, src) ⇒ Object
86 87 88 |
# File 'lib/dynamic_locals/ast_rewriter.rb', line 86 def replace_offsets(wrapped_start, wrapped_end, src) @replacements << Replacement.new((wrapped_start - @offset)...(wrapped_end - @offset), src) end |