Module: Guardrails::ErbParser
- Defined in:
- lib/guardrails/erb_parser.rb
Overview
Thin wrapper around the Herb ERB parser. Centralizes the call so:
- Detectors get a stable interface even if Herb's API drifts
- Parse failures degrade gracefully (return an empty document
rather than crashing the whole audit)
- Future caching / incremental-parse hooks have a single point
of attachment
Detectors should walk ‘result.document`, an `Herb::AST::DocumentNode`. Position info on every node is `node.location`, which exposes a `start { line, column }` and `end { line, column }` (1-indexed lines, 0-indexed columns from Herb).
Defined Under Namespace
Classes: Result
Class Method Summary collapse
-
.compact_children(node) ⇒ Object
Herb nodes expose either ‘compact_child_nodes` (preferred — skips nils and unwraps single-child wrappers) or just `children`.
-
.each_node(node) {|node| ... } ⇒ Object
Walk an AST node depth-first, yielding each descendant (including the root).
-
.empty_result(source, errors) ⇒ Object
Build a Result around an empty parsed document so detectors that walk ‘result.document` see no elements rather than crashing.
-
.parse(source) ⇒ Object
Parse ERB source text.
-
.start_position(node) ⇒ Object
Convert a Herb location to (line, column) for a node’s start.
Class Method Details
.compact_children(node) ⇒ Object
Herb nodes expose either ‘compact_child_nodes` (preferred — skips nils and unwraps single-child wrappers) or just `children`.
73 74 75 76 77 78 79 80 81 |
# File 'lib/guardrails/erb_parser.rb', line 73 def compact_children(node) if node.respond_to?(:compact_child_nodes) Array(node.compact_child_nodes) elsif node.respond_to?(:children) Array(node.children) else [] end end |
.each_node(node) {|node| ... } ⇒ Object
Walk an AST node depth-first, yielding each descendant (including the root). Callers filter by ‘node.class` or `node.tag_name`.
64 65 66 67 68 69 |
# File 'lib/guardrails/erb_parser.rb', line 64 def each_node(node, &block) return enum_for(:each_node, node) unless block_given? yield node compact_children(node).each { |child| each_node(child, &block) } end |
.empty_result(source, errors) ⇒ Object
Build a Result around an empty parsed document so detectors that walk ‘result.document` see no elements rather than crashing.
50 51 52 53 54 55 56 57 |
# File 'lib/guardrails/erb_parser.rb', line 50 def empty_result(source, errors) empty = Herb.parse("") Result.new(document: empty.value, errors: Array(errors), source: source) rescue StandardError # If Herb can't even parse "", give back a Result whose document # responds to compact_child_nodes with []. Use a Struct-as-stub. Result.new(document: NULL_DOCUMENT, errors: Array(errors), source: source) end |
.parse(source) ⇒ Object
Parse ERB source text. Returns a Result regardless of parse success — Herb crashes or value-less results both degrade to a safe empty-document Result so callers can always traverse ‘result.document` without nil-checking. Callers that care about strictness should check `result.success?` and inspect `result.errors`.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/guardrails/erb_parser.rb', line 32 def parse(source) herb_result = Herb.parse(source) document = herb_result.value if document.nil? empty_result(source, ["herb returned no document for source"]) else Result.new( document: document, errors: Array(herb_result.errors), source: source ) end rescue StandardError => e empty_result(source, ["#{e.class}: #{e.}"]) end |
.start_position(node) ⇒ Object
Convert a Herb location to (line, column) for a node’s start. Herb uses 1-indexed lines and 0-indexed columns; Guardrails violations use 1-indexed columns. This adjusts for that.
86 87 88 89 |
# File 'lib/guardrails/erb_parser.rb', line 86 def start_position(node) loc = node.location [loc.start.line, loc.start.column + 1] end |