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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 157b3e70e039e8e93b06852fc5772ed1838a007b18d5e7de1eadbe65adf15f74
|
|
4
|
+
data.tar.gz: 1fc22a475a411316c609f84398db4750a9791694bcffb04e6139bb54ca0f2650
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6c0e5e74c9d63cd191424ab8804e712b46948f02e98eb40d30c777626c0b991a490ce792a6cb36afd41b4b322942d4e0e635f9cbcd30f9ed617d3a369602a11d
|
|
7
|
+
data.tar.gz: e9e08f1c2cadd8626accc273de42629b34977e4f3e3051ab1d1351c8d66b2ae39d8f97abe88d02f37e696cf0c8d95a6064b038be5a061d0f4900e66060992976
|
data/exe/herb-lint-rb
ADDED
data/js/host_shim.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Minimal UTF-8 TextDecoder/TextEncoder for bare V8 embeddings (e.g.
|
|
2
|
+
// mini_racer), which provide ECMAScript but not WHATWG host APIs.
|
|
3
|
+
//
|
|
4
|
+
// Strict on purpose: real TextDecoder throws on non-buffer input rather
|
|
5
|
+
// than coercing it. A shim that coerces turns loud failures into silent
|
|
6
|
+
// ones — decode("a string") must throw, not return empty output.
|
|
7
|
+
(function (global) {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
function toUint8Array(input) {
|
|
11
|
+
if (input instanceof Uint8Array) {
|
|
12
|
+
return input;
|
|
13
|
+
}
|
|
14
|
+
if (input instanceof ArrayBuffer) {
|
|
15
|
+
return new Uint8Array(input);
|
|
16
|
+
}
|
|
17
|
+
if (ArrayBuffer.isView(input)) {
|
|
18
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
19
|
+
}
|
|
20
|
+
throw new TypeError(
|
|
21
|
+
"Failed to execute 'decode': The provided value is not of type " +
|
|
22
|
+
"'(ArrayBuffer or ArrayBufferView)'"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class TextDecoder {
|
|
27
|
+
constructor(encoding) {
|
|
28
|
+
if (encoding !== undefined && encoding !== "utf-8" && encoding !== "utf8") {
|
|
29
|
+
throw new RangeError("TextDecoder only supports utf-8");
|
|
30
|
+
}
|
|
31
|
+
this.encoding = "utf-8";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
decode(input) {
|
|
35
|
+
const bytes = toUint8Array(input);
|
|
36
|
+
let result = "";
|
|
37
|
+
let i = 0;
|
|
38
|
+
|
|
39
|
+
while (i < bytes.length) {
|
|
40
|
+
const byte1 = bytes[i];
|
|
41
|
+
let codePoint;
|
|
42
|
+
let extraBytes;
|
|
43
|
+
|
|
44
|
+
if (byte1 < 0x80) {
|
|
45
|
+
codePoint = byte1;
|
|
46
|
+
extraBytes = 0;
|
|
47
|
+
} else if ((byte1 & 0xe0) === 0xc0) {
|
|
48
|
+
codePoint = byte1 & 0x1f;
|
|
49
|
+
extraBytes = 1;
|
|
50
|
+
} else if ((byte1 & 0xf0) === 0xe0) {
|
|
51
|
+
codePoint = byte1 & 0x0f;
|
|
52
|
+
extraBytes = 2;
|
|
53
|
+
} else if ((byte1 & 0xf8) === 0xf0) {
|
|
54
|
+
codePoint = byte1 & 0x07;
|
|
55
|
+
extraBytes = 3;
|
|
56
|
+
} else {
|
|
57
|
+
throw new TypeError("Invalid UTF-8 byte sequence");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let j = 0; j < extraBytes; j++) {
|
|
61
|
+
i++;
|
|
62
|
+
const nextByte = bytes[i];
|
|
63
|
+
if (nextByte === undefined || (nextByte & 0xc0) !== 0x80) {
|
|
64
|
+
throw new TypeError("Invalid UTF-8 continuation byte");
|
|
65
|
+
}
|
|
66
|
+
codePoint = (codePoint << 6) | (nextByte & 0x3f);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (codePoint > 0xffff) {
|
|
70
|
+
codePoint -= 0x10000;
|
|
71
|
+
result += String.fromCharCode(
|
|
72
|
+
0xd800 + (codePoint >> 10),
|
|
73
|
+
0xdc00 + (codePoint & 0x3ff)
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
result += String.fromCharCode(codePoint);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class TextEncoder {
|
|
87
|
+
constructor() {
|
|
88
|
+
this.encoding = "utf-8";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
encode(input) {
|
|
92
|
+
if (typeof input !== "string") {
|
|
93
|
+
throw new TypeError(
|
|
94
|
+
"Failed to execute 'encode': parameter 1 is not of type 'string'."
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const bytes = [];
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < input.length; i++) {
|
|
101
|
+
let codePoint = input.charCodeAt(i);
|
|
102
|
+
|
|
103
|
+
if (codePoint >= 0xd800 && codePoint <= 0xdbff && i + 1 < input.length) {
|
|
104
|
+
const next = input.charCodeAt(i + 1);
|
|
105
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
106
|
+
codePoint = (codePoint - 0xd800) * 0x400 + (next - 0xdc00) + 0x10000;
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (codePoint < 0x80) {
|
|
112
|
+
bytes.push(codePoint);
|
|
113
|
+
} else if (codePoint < 0x800) {
|
|
114
|
+
bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
|
|
115
|
+
} else if (codePoint < 0x10000) {
|
|
116
|
+
bytes.push(
|
|
117
|
+
0xe0 | (codePoint >> 12),
|
|
118
|
+
0x80 | ((codePoint >> 6) & 0x3f),
|
|
119
|
+
0x80 | (codePoint & 0x3f)
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
bytes.push(
|
|
123
|
+
0xf0 | (codePoint >> 18),
|
|
124
|
+
0x80 | ((codePoint >> 12) & 0x3f),
|
|
125
|
+
0x80 | ((codePoint >> 6) & 0x3f),
|
|
126
|
+
0x80 | (codePoint & 0x3f)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return new Uint8Array(bytes);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
global.TextDecoder = TextDecoder;
|
|
136
|
+
global.TextEncoder = TextEncoder;
|
|
137
|
+
})(typeof globalThis !== "undefined" ? globalThis : this);
|
data/js/ruby_backend.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Wires the vendored @herb-tools/linter bundle to Ruby. Must load after
|
|
2
|
+
// vendor/herb-linter.js and after the six rbXxx callbacks below are
|
|
3
|
+
// attached (Bridge#boot handles the ordering).
|
|
4
|
+
//
|
|
5
|
+
// HerbBackend (from @herb-tools/core) must be subclassed: its `version`
|
|
6
|
+
// getter calls the abstract backendVersion(), which the base class
|
|
7
|
+
// doesn't implement. Instantiating HerbBackend directly fails.
|
|
8
|
+
var libHerbBackend = {
|
|
9
|
+
parse: function (source, options) {
|
|
10
|
+
return JSON.parse(rbParse(source, options));
|
|
11
|
+
},
|
|
12
|
+
lex: function (source) {
|
|
13
|
+
return JSON.parse(rbLex(source));
|
|
14
|
+
},
|
|
15
|
+
extractRuby: function (source, options) {
|
|
16
|
+
return rbExtractRuby(source, options);
|
|
17
|
+
},
|
|
18
|
+
extractHTML: function (source) {
|
|
19
|
+
return rbExtractHTML(source);
|
|
20
|
+
},
|
|
21
|
+
parseRuby: function (source) {
|
|
22
|
+
return rbParseRuby(source);
|
|
23
|
+
},
|
|
24
|
+
version: function () {
|
|
25
|
+
return rbVersion();
|
|
26
|
+
},
|
|
27
|
+
diff: function () {
|
|
28
|
+
throw new Error("diff is not implemented by the Ruby backend");
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// rbParse (via ResultEnvelope#inject_prism_nodes) injects prism_node bytes
|
|
33
|
+
// produced by Ruby's Prism.dump, which always serializes a whole
|
|
34
|
+
// ProgramNode — there is no public API to dump an arbitrary sub-node
|
|
35
|
+
// directly. But every prism_nodes-dependent rule (see CHARTER.md) expects
|
|
36
|
+
// an ERB*Node's prismNode to BE the single embedded-Ruby expression node
|
|
37
|
+
// itself (e.g. isAssignmentNode checks prismNode.constructor.name), not a
|
|
38
|
+
// Program wrapping it. Unwrap here, once, for every ERB node class that
|
|
39
|
+
// defines the prismNode getter, rather than patching the vendored bundle.
|
|
40
|
+
// Only unwraps when the parse yielded exactly one top-level statement —
|
|
41
|
+
// the case single-tag Ruby content always produces — leaving anything
|
|
42
|
+
// else (multiple ';'-separated statements in one tag) as the ProgramNode,
|
|
43
|
+
// same as an unhandled edge case would fall back to.
|
|
44
|
+
Object.keys(HerbLinter).forEach(function (name) {
|
|
45
|
+
if (!/^ERB.*Node$/.test(name)) return;
|
|
46
|
+
|
|
47
|
+
var proto = HerbLinter[name] && HerbLinter[name].prototype;
|
|
48
|
+
var descriptor = proto && Object.getOwnPropertyDescriptor(proto, "prismNode");
|
|
49
|
+
if (!descriptor || typeof descriptor.get !== "function") return;
|
|
50
|
+
|
|
51
|
+
var originalGet = descriptor.get;
|
|
52
|
+
|
|
53
|
+
Object.defineProperty(proto, "prismNode", {
|
|
54
|
+
configurable: true,
|
|
55
|
+
enumerable: descriptor.enumerable,
|
|
56
|
+
get: function () {
|
|
57
|
+
var raw = originalGet.call(this);
|
|
58
|
+
var body = raw && raw.constructor && raw.constructor.name === "ProgramNode" && raw.statements && raw.statements.body;
|
|
59
|
+
|
|
60
|
+
return body && body.length === 1 ? body[0] : raw;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
class RubyBackend extends HerbLinter.HerbBackend {
|
|
66
|
+
backendVersion() {
|
|
67
|
+
return "mini_racer";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A Promise returned directly from a Ruby<->JS call marshals to Ruby as
|
|
72
|
+
// {} — mini_racer doesn't drive V8's microtask queue on its own timeline,
|
|
73
|
+
// only between separate eval/call invocations. So boot is a two-step
|
|
74
|
+
// handshake: this call kicks off the async load without awaiting it;
|
|
75
|
+
// Bridge#boot observes __herbEmbeddedBridge.ready in a second, separate
|
|
76
|
+
// call, by which point the microtask queue has drained and the promise
|
|
77
|
+
// has resolved.
|
|
78
|
+
var __herbEmbeddedBridge = {
|
|
79
|
+
instance: new RubyBackend(function () {
|
|
80
|
+
return Promise.resolve(libHerbBackend);
|
|
81
|
+
}),
|
|
82
|
+
ready: false,
|
|
83
|
+
error: null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
__herbEmbeddedBridge.instance
|
|
87
|
+
.load()
|
|
88
|
+
.then(function () {
|
|
89
|
+
__herbEmbeddedBridge.ready = true;
|
|
90
|
+
})
|
|
91
|
+
.catch(function (e) {
|
|
92
|
+
__herbEmbeddedBridge.error = String(e && e.message ? e.message : e);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function __herbRuleNames() {
|
|
96
|
+
return HerbLinter.rules.map(function (ruleClass) {
|
|
97
|
+
return ruleClass.ruleName;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Rule selection happens before execution: Linter's constructor takes an
|
|
102
|
+
// explicit rules array, so an unselected rule is never instantiated or
|
|
103
|
+
// checked, not merely filtered out of the offenses it already produced.
|
|
104
|
+
// With no explicit selection, match upstream's own default: only rules
|
|
105
|
+
// enabled by their own defaultConfig (Linter.filterRulesByConfig with no
|
|
106
|
+
// user config still filters on that), not every available rule class.
|
|
107
|
+
// An explicit selection bypasses that filtering entirely — matching
|
|
108
|
+
// --only's real semantics — so a caller can still opt into a
|
|
109
|
+
// not-enabled-by-default rule by naming it.
|
|
110
|
+
function __herbSelectRules(ruleNames) {
|
|
111
|
+
var wanted = ruleNames && ruleNames.length ? ruleNames : null;
|
|
112
|
+
|
|
113
|
+
if (!wanted) {
|
|
114
|
+
return HerbLinter.Linter.filterRulesByConfig(HerbLinter.rules).enabled;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return HerbLinter.rules.filter(function (ruleClass) {
|
|
118
|
+
return wanted.indexOf(ruleClass.ruleName) !== -1;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Registers a custom rule from Ruby-rewritten source (see
|
|
123
|
+
// CustomRuleLoader.rewrite): an IIFE expression that destructures its
|
|
124
|
+
// base class from HerbLinter and returns the rule class. A rule sharing
|
|
125
|
+
// a ruleName with an existing one (built-in or previously-registered
|
|
126
|
+
// custom) replaces it in place, matching upstream's override behavior —
|
|
127
|
+
// the caller (CustomRuleLoader#load_all) is told via `overrode` so it can
|
|
128
|
+
// warn on the Ruby side.
|
|
129
|
+
function __herbRegisterCustomRule(rewrittenSource, path) {
|
|
130
|
+
var RuleClass;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
RuleClass = eval(rewrittenSource);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
throw new Error("Failed to evaluate custom rule at " + path + ": " + (e && e.message ? e.message : String(e)));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!RuleClass || typeof RuleClass !== "function" || typeof RuleClass.ruleName !== "string" || typeof RuleClass.prototype.check !== "function") {
|
|
139
|
+
throw new Error("No valid default export found in " + path + ". Custom rules must use default export.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var existingIndex = HerbLinter.rules.findIndex(function (ruleClass) {
|
|
143
|
+
return ruleClass.ruleName === RuleClass.ruleName;
|
|
144
|
+
});
|
|
145
|
+
var overrode = existingIndex !== -1;
|
|
146
|
+
|
|
147
|
+
if (overrode) {
|
|
148
|
+
HerbLinter.rules.splice(existingIndex, 1, RuleClass);
|
|
149
|
+
} else {
|
|
150
|
+
HerbLinter.rules.push(RuleClass);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { ruleName: RuleClass.ruleName, overrode: overrode };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// A single Linter across all selected rules, matching upstream's own
|
|
157
|
+
// lint-then-fix composition (Linter#autofix mutates a running source as
|
|
158
|
+
// it walks offenses from every selected rule together, unlike #lint's
|
|
159
|
+
// per-rule crash isolation). The parse-doesn't-break safety check lives
|
|
160
|
+
// on the Ruby side (Bridge#autofix), since it just needs Herb.parse.
|
|
161
|
+
function __herbAutofix(source, file, ruleNames, includeUnsafe) {
|
|
162
|
+
var ruleClasses = __herbSelectRules(ruleNames);
|
|
163
|
+
var context = { fileName: file, filename: file };
|
|
164
|
+
var linter = new HerbLinter.Linter(__herbEmbeddedBridge.instance, ruleClasses, undefined, HerbLinter.rules);
|
|
165
|
+
var result = linter.autofix(source, context, undefined, { includeUnsafe: !!includeUnsafe });
|
|
166
|
+
|
|
167
|
+
return JSON.stringify({ source: result.source, fixed: result.fixed });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// One Linter per selected rule, each in its own try/catch: a rule that
|
|
171
|
+
// throws must not abort the run (spike 2 finding) — the real Linter#lint
|
|
172
|
+
// has no such isolation internally, since it runs every selected rule's
|
|
173
|
+
// check() in one uncaught loop.
|
|
174
|
+
//
|
|
175
|
+
// The 4th constructor arg (allAvailableRules) must be the full registry,
|
|
176
|
+
// not just the one rule being run: herb-disable-comment-unnecessary reads
|
|
177
|
+
// context.validRuleNames (built from Linter#getAvailableRules, which
|
|
178
|
+
// falls back to allAvailableRules) to decide whether a `herb:disable
|
|
179
|
+
// some-other-rule` comment references a real rule — omitting this arg
|
|
180
|
+
// left validRuleNames scoped to whatever single rule __herbLint happened
|
|
181
|
+
// to be running, so the rule silently never matched anything outside
|
|
182
|
+
// itself. Caught by conformance fixture coverage (herb-embedded-ag7).
|
|
183
|
+
function __herbLint(source, file, ruleNames) {
|
|
184
|
+
var ruleClasses = __herbSelectRules(ruleNames);
|
|
185
|
+
var context = { fileName: file, filename: file };
|
|
186
|
+
var offenses = [];
|
|
187
|
+
|
|
188
|
+
ruleClasses.forEach(function (ruleClass) {
|
|
189
|
+
try {
|
|
190
|
+
var linter = new HerbLinter.Linter(__herbEmbeddedBridge.instance, [ruleClass], undefined, HerbLinter.rules);
|
|
191
|
+
var result = linter.lint(source, context);
|
|
192
|
+
offenses = offenses.concat(result.offenses);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
offenses.push({
|
|
195
|
+
rule: ruleClass.ruleName,
|
|
196
|
+
message: "Rule '" + ruleClass.ruleName + "' crashed: " + (e && e.message ? e.message : String(e)),
|
|
197
|
+
severity: "error",
|
|
198
|
+
location: null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return JSON.stringify(offenses);
|
|
204
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mini_racer"
|
|
4
|
+
require_relative "../engine_adapter"
|
|
5
|
+
|
|
6
|
+
module Herb
|
|
7
|
+
module Embedded
|
|
8
|
+
module Adapters
|
|
9
|
+
# Reference EngineAdapter backed by mini_racer (bundled V8).
|
|
10
|
+
class MiniRacer < EngineAdapter
|
|
11
|
+
def initialize
|
|
12
|
+
super
|
|
13
|
+
@context = ::MiniRacer::Context.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def load(source)
|
|
17
|
+
@context.eval(source)
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def attach(name, &block)
|
|
22
|
+
wrapped = proc do |*args|
|
|
23
|
+
result = block.call(*args)
|
|
24
|
+
result.is_a?(Binary) ? ::MiniRacer::Binary.new(result.raw) : result
|
|
25
|
+
end
|
|
26
|
+
@context.attach(name, wrapped)
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(function, *args)
|
|
31
|
+
converted = args.map { |arg| arg.is_a?(Binary) ? arg.raw.bytes : arg }
|
|
32
|
+
@context.call(function, *converted)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def dispose
|
|
36
|
+
@context.dispose
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "prism"
|
|
5
|
+
require_relative "../embedded"
|
|
6
|
+
require_relative "engine_adapter"
|
|
7
|
+
require_relative "result_envelope"
|
|
8
|
+
require_relative "diagnostic"
|
|
9
|
+
|
|
10
|
+
module Herb
|
|
11
|
+
module Embedded
|
|
12
|
+
# Boots the JS engine: loads the host shim and vendored bundle,
|
|
13
|
+
# attaches the six Ruby callbacks HerbBackend needs, then loads the
|
|
14
|
+
# RubyBackend subclass and waits for its async init to resolve.
|
|
15
|
+
class Bridge
|
|
16
|
+
# Rescuing Herb::Embedded::Error catches any of these without also
|
|
17
|
+
# catching unrelated bugs.
|
|
18
|
+
class VersionMismatchError < Error; end
|
|
19
|
+
class NotBootedError < Error; end
|
|
20
|
+
class BootError < Error; end
|
|
21
|
+
|
|
22
|
+
HOST_SHIM_PATH = File.expand_path("../../../js/host_shim.js", __dir__)
|
|
23
|
+
RUBY_BACKEND_PATH = File.expand_path("../../../js/ruby_backend.js", __dir__)
|
|
24
|
+
|
|
25
|
+
READY_CHECK_JS = <<~JS
|
|
26
|
+
function __herbEmbeddedReady() { return __herbEmbeddedBridge.ready; }
|
|
27
|
+
function __herbEmbeddedError() { return __herbEmbeddedBridge.error; }
|
|
28
|
+
function __herbEmbeddedVersion() { return __herbEmbeddedBridge.instance.version; }
|
|
29
|
+
JS
|
|
30
|
+
|
|
31
|
+
def initialize(adapter:, bundle:)
|
|
32
|
+
@adapter = adapter
|
|
33
|
+
@bundle = bundle
|
|
34
|
+
@booted = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def boot
|
|
38
|
+
# Herb is pre-1.0: the major version never moves (0.8 -> 0.9 -> 0.10
|
|
39
|
+
# are all "0"), so semver-range gating can't distinguish a validated
|
|
40
|
+
# version from an unvalidated one. Gate on the recorded set instead.
|
|
41
|
+
unless @bundle.herb_versions.include?(::Herb::VERSION)
|
|
42
|
+
raise VersionMismatchError,
|
|
43
|
+
"herb #{::Herb::VERSION} has not been validated against this bundle " \
|
|
44
|
+
"(validated versions: #{@bundle.herb_versions.join(", ")})"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@adapter.load(File.read(HOST_SHIM_PATH))
|
|
48
|
+
@adapter.load(@bundle.source)
|
|
49
|
+
attach_callbacks
|
|
50
|
+
@adapter.load(File.read(RUBY_BACKEND_PATH))
|
|
51
|
+
@adapter.load(READY_CHECK_JS)
|
|
52
|
+
|
|
53
|
+
unless @adapter.call("__herbEmbeddedReady")
|
|
54
|
+
raise BootError, "Bridge failed to boot: #{@adapter.call("__herbEmbeddedError")}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@booted = true
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def backend_version
|
|
62
|
+
ensure_booted!
|
|
63
|
+
@adapter.call("__herbEmbeddedVersion")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def rule_names
|
|
67
|
+
ensure_booted!
|
|
68
|
+
@adapter.call("__herbRuleNames")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def lint(source, file:, rules: nil)
|
|
72
|
+
ensure_booted!
|
|
73
|
+
offenses = JSON.parse(@adapter.call("__herbLint", source, file, rules))
|
|
74
|
+
offenses.map { |offense| Diagnostic.from_js(offense, file: file) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_custom_rule(source, path)
|
|
78
|
+
ensure_booted!
|
|
79
|
+
@adapter.call("__herbRegisterCustomRule", source, path)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A formatter-adjacent tool must never write source that fails to
|
|
83
|
+
# parse into a user's working tree. If the fixed output fails to
|
|
84
|
+
# parse while the original input parsed cleanly, the fix is
|
|
85
|
+
# discarded rather than trusted.
|
|
86
|
+
def autofix(source, file:, rules: nil, unsafe: false)
|
|
87
|
+
ensure_booted!
|
|
88
|
+
raw = JSON.parse(@adapter.call("__herbAutofix", source, file, rules, unsafe))
|
|
89
|
+
|
|
90
|
+
if source_parses?(source) && !source_parses?(raw["source"])
|
|
91
|
+
return { source: source, applied: [], discarded: true }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
applied = raw["fixed"].map { |offense| Diagnostic.from_js(offense, file: file) }
|
|
95
|
+
{ source: raw["source"], applied: applied, discarded: false }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def dispose
|
|
99
|
+
@adapter.dispose
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def ensure_booted!
|
|
105
|
+
raise NotBootedError, "Bridge#boot must be called before this method" unless @booted
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def attach_callbacks
|
|
109
|
+
@adapter.attach("rbParse") { |source, options| ResultEnvelope.parse(source, options || {}) }
|
|
110
|
+
@adapter.attach("rbLex") { |source| ResultEnvelope.lex(source) }
|
|
111
|
+
@adapter.attach("rbExtractRuby") { |source, options| ::Herb.extract_ruby(source, **symbolize(options)) }
|
|
112
|
+
@adapter.attach("rbExtractHTML") { |source| ::Herb.extract_html(source) }
|
|
113
|
+
@adapter.attach("rbParseRuby") { |source| EngineAdapter.binary(::Prism.dump(source)) }
|
|
114
|
+
@adapter.attach("rbVersion") { ::Herb.version }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def symbolize(hash)
|
|
118
|
+
(hash || {}).transform_keys(&:to_sym)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def source_parses?(source)
|
|
122
|
+
JSON.parse(ResultEnvelope.parse(source, {}))["errors"].empty?
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Herb
|
|
4
|
+
module Embedded
|
|
5
|
+
# The vendored @herb-tools/linter JS bundle (see rakelib/bundle.rake).
|
|
6
|
+
module Bundle
|
|
7
|
+
LINTER_VERSION = "0.10.3"
|
|
8
|
+
HERB_VERSIONS = ["0.10.3"].freeze
|
|
9
|
+
|
|
10
|
+
VENDOR_PATH = File.expand_path("../../../vendor/herb-linter.js", __dir__)
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def source
|
|
15
|
+
File.read(VENDOR_PATH)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def linter_version
|
|
19
|
+
LINTER_VERSION
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def herb_versions
|
|
23
|
+
HERB_VERSIONS
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|