herb-embedded 0.10.3.0
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 +7 -0
- data/exe/herb-lint-rb +7 -0
- data/js/host_shim.js +137 -0
- data/js/ruby_backend.js +204 -0
- data/lib/herb/embedded/adapters/mini_racer.rb +41 -0
- data/lib/herb/embedded/bridge.rb +126 -0
- data/lib/herb/embedded/bundle.rb +27 -0
- data/lib/herb/embedded/cli.rb +158 -0
- data/lib/herb/embedded/config.rb +51 -0
- data/lib/herb/embedded/custom_rule_loader.rb +95 -0
- data/lib/herb/embedded/diagnostic.rb +69 -0
- data/lib/herb/embedded/engine_adapter.rb +35 -0
- data/lib/herb/embedded/formatters/detailed.rb +29 -0
- data/lib/herb/embedded/formatters/github.rb +21 -0
- data/lib/herb/embedded/formatters/json.rb +34 -0
- data/lib/herb/embedded/formatters/simple.rb +28 -0
- data/lib/herb/embedded/formatters.rb +29 -0
- data/lib/herb/embedded/lint_result.rb +15 -0
- data/lib/herb/embedded/report.rb +40 -0
- data/lib/herb/embedded/result_envelope.rb +147 -0
- data/lib/herb/embedded/runner.rb +79 -0
- data/lib/herb/embedded/version.rb +7 -0
- data/lib/herb/embedded.rb +10 -0
- data/vendor/herb-linter.js +66574 -0
- metadata +92 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "herb"
|
|
4
|
+
require "json"
|
|
5
|
+
require "prism"
|
|
6
|
+
|
|
7
|
+
module Herb
|
|
8
|
+
module Embedded
|
|
9
|
+
# Herb::ParseResult#to_json returns an inspect string, not the wire
|
|
10
|
+
# format @herb-tools/core's ParseResult.from() expects. This builds
|
|
11
|
+
# that envelope by hand from the pieces that do serialize correctly.
|
|
12
|
+
module ResultEnvelope
|
|
13
|
+
# Allowlist of Herb::ParserOptions keys safe to forward from
|
|
14
|
+
# caller-supplied options. Deliberately excludes prism_nodes,
|
|
15
|
+
# prism_nodes_deep, and prism_program: asking Herb.parse itself to
|
|
16
|
+
# embed prism_node populates it with a raw ASCII-8BIT String, and
|
|
17
|
+
# forwarding that through raises JSON::GeneratorError. Both are
|
|
18
|
+
# instead handled below by computing JSON-safe byte arrays
|
|
19
|
+
# ourselves. Also excludes timeout and max_errors (timing/error-cap
|
|
20
|
+
# options, not shape).
|
|
21
|
+
FORWARDABLE_OPTIONS = %i[
|
|
22
|
+
strict
|
|
23
|
+
track_whitespace
|
|
24
|
+
analyze
|
|
25
|
+
action_view_helpers
|
|
26
|
+
transform_conditionals
|
|
27
|
+
render_nodes
|
|
28
|
+
strict_locals
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def parse(source, options_hash = {})
|
|
34
|
+
options_hash = (options_hash || {}).transform_keys(&:to_sym)
|
|
35
|
+
envelope = build_envelope(source, options_hash)
|
|
36
|
+
|
|
37
|
+
return envelope.to_json unless options_hash[:prism_nodes] || options_hash[:prism_nodes_deep]
|
|
38
|
+
|
|
39
|
+
with_injected_prism_nodes(envelope, source)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def build_envelope(source, options_hash)
|
|
43
|
+
result = Herb.parse(source, **forwardable(options_hash))
|
|
44
|
+
|
|
45
|
+
value_hash = result.value.to_hash
|
|
46
|
+
value_hash[:prism_node] = prism_program_bytes(source) if options_hash[:prism_program]
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
value: value_hash,
|
|
50
|
+
source: result.source,
|
|
51
|
+
warnings: result.warnings,
|
|
52
|
+
errors: result.errors,
|
|
53
|
+
options: result.options.to_h,
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
private_class_method :build_envelope
|
|
57
|
+
|
|
58
|
+
def lex(source)
|
|
59
|
+
result = Herb.lex(source)
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
tokens: result.value,
|
|
63
|
+
source: result.source,
|
|
64
|
+
warnings: result.warnings,
|
|
65
|
+
errors: result.errors,
|
|
66
|
+
}.to_json
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# value_hash's children are still live Herb::AST::Node objects
|
|
70
|
+
# (#to_hash is shallow), so per-node injection needs them as plain
|
|
71
|
+
# Hashes first. A JSON round-trip is the simplest way to get that
|
|
72
|
+
# without hand-walking Node#child_nodes ourselves.
|
|
73
|
+
def with_injected_prism_nodes(envelope, source)
|
|
74
|
+
parsed_envelope = JSON.parse(envelope.to_json)
|
|
75
|
+
inject_prism_nodes(parsed_envelope["value"], source)
|
|
76
|
+
parsed_envelope.to_json
|
|
77
|
+
end
|
|
78
|
+
private_class_method :with_injected_prism_nodes
|
|
79
|
+
|
|
80
|
+
def forwardable(options_hash)
|
|
81
|
+
options_hash.each_with_object({}) do |(key, value), forwarded|
|
|
82
|
+
symbol_key = key.to_sym
|
|
83
|
+
forwarded[symbol_key] = value if FORWARDABLE_OPTIONS.include?(symbol_key)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
private_class_method :forwardable
|
|
87
|
+
|
|
88
|
+
# @herb-tools/core's DocumentNode#prismNode getter deserializes
|
|
89
|
+
# prism_node bytes against the node's own (whole-file) `source`, so
|
|
90
|
+
# the bytes must come from parsing something byte-length-identical
|
|
91
|
+
# to source with Ruby content at the same offsets — exactly what
|
|
92
|
+
# Herb.extract_ruby produces (non-Ruby content blanked, not
|
|
93
|
+
# stripped). A plain Array of bytes (not the ASCII-8BIT String
|
|
94
|
+
# Prism.dump returns) is what keeps this JSON-safe.
|
|
95
|
+
def prism_program_bytes(source)
|
|
96
|
+
Prism.dump(Herb.extract_ruby(source)).bytes
|
|
97
|
+
end
|
|
98
|
+
private_class_method :prism_program_bytes
|
|
99
|
+
|
|
100
|
+
# Every AST_ERB_* node (ERBContentNode, ERBBlockNode, ERBIfNode,
|
|
101
|
+
# ...) carries its own embedded-Ruby snippet in a `content` token
|
|
102
|
+
# with a byte `range` into the whole file. Unlike prism_program's
|
|
103
|
+
# single whole-document parse, each of these needs its own Prism
|
|
104
|
+
# parse scoped to just that snippet — but still offset-correct
|
|
105
|
+
# against the whole-file `source`, since that's what every
|
|
106
|
+
# ERB*Node#prismNode getter deserializes against (ruby_backend.js
|
|
107
|
+
# unwraps the resulting single-statement ProgramNode down to the
|
|
108
|
+
# inner expression node the vendored rules actually expect).
|
|
109
|
+
def inject_prism_nodes(node, source)
|
|
110
|
+
case node
|
|
111
|
+
when Hash
|
|
112
|
+
inject_prism_node_for(node, source)
|
|
113
|
+
node.each_value { |value| inject_prism_nodes(value, source) }
|
|
114
|
+
when Array
|
|
115
|
+
node.each { |value| inject_prism_nodes(value, source) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
private_class_method :inject_prism_nodes
|
|
119
|
+
|
|
120
|
+
def inject_prism_node_for(node, source)
|
|
121
|
+
return unless node["type"].is_a?(String) && node["type"].start_with?("AST_ERB_")
|
|
122
|
+
|
|
123
|
+
range = node.dig("content", "range")
|
|
124
|
+
return unless range.is_a?(Array) && range.length == 2
|
|
125
|
+
|
|
126
|
+
node["prism_node"] = prism_nodes_bytes(source, range[0], range[1])
|
|
127
|
+
end
|
|
128
|
+
private_class_method :inject_prism_node_for
|
|
129
|
+
|
|
130
|
+
# Blanks (space, newlines preserved) every byte outside [from, to)
|
|
131
|
+
# so the one node's own Ruby content parses alone — at the correct
|
|
132
|
+
# absolute offset — rather than pulling in unrelated HTML or other
|
|
133
|
+
# ERB tags' Ruby.
|
|
134
|
+
def prism_nodes_bytes(source, from, to)
|
|
135
|
+
bytes = source.b.bytes
|
|
136
|
+
bytes.each_index do |i|
|
|
137
|
+
next if i >= from && i < to
|
|
138
|
+
|
|
139
|
+
bytes[i] = 0x20 unless bytes[i] == 0x0A
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Prism.dump(bytes.pack("C*").force_encoding(source.encoding)).bytes
|
|
143
|
+
end
|
|
144
|
+
private_class_method :prism_nodes_bytes
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require_relative "report"
|
|
5
|
+
require_relative "lint_result"
|
|
6
|
+
require_relative "custom_rule_loader"
|
|
7
|
+
|
|
8
|
+
module Herb
|
|
9
|
+
module Embedded
|
|
10
|
+
# The Ruby replacement for cli.js's filesystem role: discovers files,
|
|
11
|
+
# iterates, and aggregates results. Formatters are pure functions of
|
|
12
|
+
# the Report this produces.
|
|
13
|
+
class Runner
|
|
14
|
+
def initialize(root:, config:, bridge:)
|
|
15
|
+
@root = root
|
|
16
|
+
@config = config
|
|
17
|
+
@bridge = bridge
|
|
18
|
+
@custom_rules_loaded = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run(paths = nil, rules: nil)
|
|
22
|
+
load_custom_rules!
|
|
23
|
+
|
|
24
|
+
Report.new.tap do |report|
|
|
25
|
+
files_for(paths).each do |absolute_path|
|
|
26
|
+
file = relative_path(absolute_path)
|
|
27
|
+
diagnostics = @bridge.lint(File.read(absolute_path), file: file, rules: rules)
|
|
28
|
+
report.add(LintResult.new(file: file, diagnostics: diagnostics))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fix(paths = nil, rules: nil, unsafe: false)
|
|
34
|
+
load_custom_rules!
|
|
35
|
+
|
|
36
|
+
Report.new.tap do |report|
|
|
37
|
+
files_for(paths).each do |absolute_path|
|
|
38
|
+
file = relative_path(absolute_path)
|
|
39
|
+
original_source = File.read(absolute_path)
|
|
40
|
+
result = @bridge.autofix(original_source, file: file, rules: rules, unsafe: unsafe)
|
|
41
|
+
|
|
42
|
+
File.write(absolute_path, result[:source]) if result[:source] != original_source
|
|
43
|
+
|
|
44
|
+
diagnostics = @bridge.lint(result[:source], file: file, rules: rules)
|
|
45
|
+
report.add(LintResult.new(file: file, diagnostics: diagnostics))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Idempotent and safe to call ahead of #run/#fix — e.g. so a caller
|
|
51
|
+
# can validate rule names (like a CLI's --only flag) against
|
|
52
|
+
# Bridge#rule_names with custom rules already registered.
|
|
53
|
+
def load_custom_rules!
|
|
54
|
+
return if @custom_rules_loaded
|
|
55
|
+
|
|
56
|
+
CustomRuleLoader.new(root: @root, bridge: @bridge).load_all
|
|
57
|
+
@custom_rules_loaded = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def files_for(paths)
|
|
63
|
+
return discover_files unless paths
|
|
64
|
+
|
|
65
|
+
paths.map { |path| File.expand_path(path, @root) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def discover_files
|
|
69
|
+
included = @config.include_globs.flat_map { |glob| Dir.glob(File.join(@root, glob)) }
|
|
70
|
+
excluded = @config.exclude_globs.flat_map { |glob| Dir.glob(File.join(@root, glob)) }
|
|
71
|
+
(included - excluded).sort
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def relative_path(absolute_path)
|
|
75
|
+
Pathname.new(absolute_path).relative_path_from(Pathname.new(@root)).to_s
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|