Module: RuboCop::Cop::ReparsedEquivalence
| Relationships & Source Files | |
| Extension / Inclusion / Inheritance Descendants | |
|
Included In:
| |
| Defined in: | lib/rubocop/cop/mixin/reparsed_equivalence.rb |
Overview
Verifies that a candidate correction of the inspected source is a syntactic no-op: the corrected source must parse successfully and produce the same AST as the original.
This turns "is this piece of syntax redundant?" questions into a parser-backed check instead of a hand-maintained enumeration of grammar rules: instead of modeling which constructs make (for example) a line continuation or a pair of parentheses significant, a cop can apply the correction and let the parser answer.
Cops implement #apply_reparse_correction(corrector, item) and call
verified_by_reparse(items), which returns the items whose corrections
are verified. Items sharing a reparse scope (the innermost method
definition or class/module body, which parse standalone and cannot
capture outer local variables) are verified together with a single
reparse, falling back to per-item verification when the batch does not
hold. Scope groups are keyed by node identity, since structurally
identical definitions in different places must not share a group.
Two hooks customize the comparison:
-
normalize_reparsed_ast(node)- loosen strict tree equality where a cop’s correction is equivalence-preserving beyond parse identity (such laws must be justified per cop; the default compares trees as-is). -
preprocess_reparsed_source(source)- rewrite both sides before parsing (e.g. to neutralizeLINEwhen a correction legitimately shifts line numbers).
FILE is handled by parsing with the original path, and fragments
containing LINE are verified by reparsing both sides so that
fragment line offsets cancel out.
Note that comments are invisible to the AST, so corrections that may touch comment text must be guarded separately by the caller.
Constant Summary
-
MAX_VERIFICATION_FRAGMENT_SIZE =
# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 43
Above this size, verification does not reparse. What happens to such items depends on
oversized; in practice only machine-generated files come near the limit.64 * 1024
Instance Method Summary
-
#corrected_scope_fragment(scope, corrected)
private
The corrections' edits are all contained within the scope, so the corrected fragment can be cut out of the corrected source by adjusting for the edits' length delta.
-
#correction_parses?(item) ⇒ Boolean
private
Whether the exact correction for
itemproduces source that still parses, without requiring an equivalent AST. - #corrections_verify?(scope, items) ⇒ Boolean private
- #item_range(item) private
-
#normalize_reparsed_ast(node)
private
Hook: loosen strict tree equality for corrections that are equivalence-preserving beyond parse identity.
- #normalized_original(original_source, original_ast) private
-
#parses_equivalently?(original_source, original_ast, corrected_source) ⇒ Boolean
private
Both sides are parsed with the original path so that
FILEresolves identically. -
#preprocess_reparsed_source(source)
private
Hook: rewrite both sides before parsing.
-
#reparse_scope(range)
private
The innermost scope that both contains
rangeand parses standalone. - #scope_groups(items) private
- #verification_too_large?(scope) ⇒ Boolean private
-
#verified_by_reparse(items, oversized: :report)
private
Returns the items whose corrections are verified.
Instance Method Details
#corrected_scope_fragment(scope, corrected) (private)
The corrections' edits are all contained within the scope, so the corrected fragment can be cut out of the corrected source by adjusting for the edits' length delta.
# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 139
def corrected_scope_fragment(scope, corrected) delta = corrected.length - processed_source.raw_source.length scope_range = scope.source_range corrected[scope_range.begin_pos...(scope_range.end_pos + delta)] end
#correction_parses?(item) ⇒ Boolean (private)
Whether the exact correction for item produces source that still
parses, without requiring an equivalent AST. For corrections that
intentionally change the tree.
#corrections_verify?(scope, items) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 121
def corrections_verify?(scope, items) corrector = Corrector.new(processed_source) items.each { |item| apply_reparse_correction(corrector, item) } corrected = corrector.process if scope fragment = corrected_scope_fragment(scope, corrected) parses_equivalently?(scope.source, scope, fragment) else parses_equivalently?(processed_source.raw_source, processed_source.ast, corrected) end rescue ::Parser::ClobberingError false end
#item_range(item) (private)
[ GitHub ]# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 93
def item_range(item) item.respond_to?(:source_range) ? item.source_range : item end
#normalize_reparsed_ast(node) (private)
Hook: loosen strict tree equality for corrections that are equivalence-preserving beyond parse identity.
# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 77
def normalize_reparsed_ast(node) node end
#normalized_original(original_source, original_ast) (private)
[ GitHub ]# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 159
def normalized_original(original_source, original_ast) original = preprocess_reparsed_source(original_source) if original != original_source || original_source.include?('__LINE__') reparsed = parse(original, processed_source.path).ast reparsed && normalize_reparsed_ast(reparsed) else @normalized_originals ||= {}.compare_by_identity @normalized_originals[original_ast] ||= normalize_reparsed_ast(original_ast) end end
#parses_equivalently?(original_source, original_ast, corrected_source) ⇒ Boolean (private)
Both sides are parsed with the original path so that FILE
resolves identically. The original side is reparsed (instead of using
the already parsed node) when preprocessing altered it or it contains
LINE, whose values would otherwise differ from a fragment parsed
at a different line offset.
# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 151
def parses_equivalently?(original_source, original_ast, corrected_source) original_normalized = normalized_original(original_source, original_ast) return false unless original_normalized rewritten = parse(preprocess_reparsed_source(corrected_source), processed_source.path) rewritten.valid_syntax? && normalize_reparsed_ast(rewritten.ast) == original_normalized end
#preprocess_reparsed_source(source) (private)
Hook: rewrite both sides before parsing.
#reparse_scope(range) (private)
The innermost scope that both contains range and parses standalone.
Method definitions and class/module bodies neither capture outer local
variables nor continue an outer expression; blocks and single
statements do not qualify, since an outer local would reparse as a
method call.
# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 102
def reparse_scope(range) node = processed_source.ast scope = nil while node.is_a?(::Parser::AST::Node) scope = node if node.type?(:any_def, :class, :module, :sclass) node = node.children.find do |child| child.is_a?(::Parser::AST::Node) && child.source_range&.contains?(range) end end scope end
#scope_groups(items) (private)
[ GitHub ]# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 86
def scope_groups(items) groups = {}.compare_by_identity items.each { |item| (groups[reparse_scope(item_range(item))] ||= []) << item } groups end
#verification_too_large?(scope) ⇒ Boolean (private)
# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 116
def verification_too_large?(scope) (scope ? scope.source_range.size : processed_source.raw_source.size) > MAX_VERIFICATION_FRAGMENT_SIZE end
#verified_by_reparse(items, oversized: :report) (private)
Returns the items whose corrections are verified. oversized controls
items whose reparse scope exceeds MAX_VERIFICATION_FRAGMENT_SIZE:
:report accepts them unverified (for cops whose offense logic stands
on its own and uses verification as a safety gate), :verify reparses
regardless of size (for cops where verification is the offense logic).
# File 'lib/rubocop/cop/mixin/reparsed_equivalence.rb', line 52
def verified_by_reparse(items, oversized: :report) scope_groups(items).flat_map do |scope, group| if (oversized == :report && verification_too_large?(scope)) || (group.size > 1 && corrections_verify?(scope, group)) group else group.select { |item| corrections_verify?(scope, [item]) } end end end