simple_english 0.2.0 → 0.4.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 +4 -4
- data/README.md +61 -39
- data/docs/RULES.md +1 -1
- data/lib/simple_english/cli.rb +53 -15
- data/lib/simple_english/client/daemon.rb +225 -0
- data/lib/simple_english/client/language_tool.rb +95 -0
- data/lib/simple_english/daemon/engine.rb +40 -0
- data/lib/simple_english/{http.rb → daemon/http.rb} +10 -4
- data/lib/simple_english/{server.rb → daemon/server.rb} +113 -7
- data/lib/simple_english/{extractor.rb → lint/extractor.rb} +6 -1
- data/lib/simple_english/lint/finding.rb +9 -0
- data/lib/simple_english/lint/lint_plan.rb +22 -0
- data/lib/simple_english/{markdown.rb → lint/markdown.rb} +28 -10
- data/lib/simple_english/lint/plain_text.rb +30 -0
- data/lib/simple_english/{result.rb → lint/result.rb} +1 -1
- data/lib/simple_english/{config.rb → setup/config.rb} +3 -2
- data/lib/simple_english/setup/fingerprint.rb +38 -0
- data/lib/simple_english/{install.rb → setup/install.rb} +19 -9
- data/lib/simple_english/setup/languagetool.rb +226 -0
- data/lib/simple_english/version.rb +1 -1
- data/lib/simple_english.rb +25 -43
- data/rules/simple-english.xml +5 -1
- metadata +42 -36
- data/lib/simple_english/client.rb +0 -165
- data/lib/simple_english/engine.rb +0 -31
- data/lib/simple_english/finding.rb +0 -8
- data/lib/simple_english/languagetool.rb +0 -143
- data/lib/simple_english/plain_text.rb +0 -18
- /data/lib/simple_english/{annotated_text.rb → lint/annotated_text.rb} +0 -0
- /data/lib/simple_english/{counts.rb → lint/counts.rb} +0 -0
- /data/lib/simple_english/{paragraph.rb → lint/paragraph.rb} +0 -0
- /data/lib/simple_english/{segment.rb → lint/segment.rb} +0 -0
- /data/lib/simple_english/{span.rb → lint/span.rb} +0 -0
- /data/lib/simple_english/{suppressions.rb → lint/suppressions.rb} +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The complete lint engine, daemon-side. Never call SimpleEnglish.lint_text
|
|
4
|
+
# here: that call probes the daemon and recurses into the server.
|
|
5
|
+
# The engine reads nothing from disk: the caller captures the enabled
|
|
6
|
+
# rule IDs once at daemon boot (Server.start) and passes them in.
|
|
7
|
+
# LintPlan owns what kind of lint an input is. This module executes
|
|
8
|
+
# the plan.
|
|
9
|
+
|
|
10
|
+
require "json"
|
|
11
|
+
|
|
12
|
+
module SimpleEnglish
|
|
13
|
+
module Engine
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def lint(text, base_url:, language: nil,
|
|
17
|
+
enabled_rules: SimpleEnglish::LanguageTool.rule_ids)
|
|
18
|
+
plan = SimpleEnglish::LintPlan.call(text, language)
|
|
19
|
+
return [] if plan.nil?
|
|
20
|
+
if plan == :prose
|
|
21
|
+
stripped = SimpleEnglish::Markdown.strip(text)
|
|
22
|
+
(SimpleEnglish::Counts.check(stripped) +
|
|
23
|
+
SimpleEnglish::LanguageTool::Client.check(stripped, base_url: base_url,
|
|
24
|
+
enabled_rules: enabled_rules))
|
|
25
|
+
.sort_by { |finding| [finding.line, finding.rule] }
|
|
26
|
+
else
|
|
27
|
+
SimpleEnglish::LanguageTool::Client.check(
|
|
28
|
+
SimpleEnglish::AnnotatedText.build(text, plan), base_url: base_url,
|
|
29
|
+
enabled_rules: enabled_rules
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def lint_json(text, base_url:, language: nil,
|
|
35
|
+
enabled_rules: SimpleEnglish::LanguageTool.rule_ids)
|
|
36
|
+
JSON.generate(lint(text, base_url: base_url, language: language,
|
|
37
|
+
enabled_rules: enabled_rules).map(&:to_h))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -43,14 +43,20 @@ module SimpleEnglish
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# One thread per request: lint can take seconds, so a slow request
|
|
46
|
-
# must not block readiness probes or other clients.
|
|
47
|
-
|
|
46
|
+
# must not block readiness probes or other clients. The enabled
|
|
47
|
+
# rule IDs and the handshake info are frozen at boot (Server.start)
|
|
48
|
+
# and passed through.
|
|
49
|
+
def handle_client(client, port:, enabled_rules:, info: {})
|
|
48
50
|
request = read_request(client)
|
|
49
51
|
if request.nil?
|
|
50
52
|
# Malformed request or immediate hangup: nothing to answer.
|
|
51
53
|
elsif request[:method] == "HEAD"
|
|
52
|
-
#
|
|
54
|
+
# Older CLIs probe with HEAD and treat any response as up.
|
|
53
55
|
write_response(client, status: 200, body: "")
|
|
56
|
+
elsif request[:method] == "GET" && request[:path] == "/"
|
|
57
|
+
# The handshake: the daemon reports its version, pid, and the
|
|
58
|
+
# gem/rules digests the client compares against its own.
|
|
59
|
+
write_response(client, status: 200, body: JSON.generate(info))
|
|
54
60
|
elsif request[:method] == "POST" && request[:path] == "/lint"
|
|
55
61
|
begin
|
|
56
62
|
# Ruling 2026-09-24: Client.lint posts form-encoded data, so the
|
|
@@ -60,7 +66,7 @@ module SimpleEnglish
|
|
|
60
66
|
write_response(client, status: 200,
|
|
61
67
|
body: SimpleEnglish::Engine.lint_json(params["text"],
|
|
62
68
|
base_url: "http://localhost:#{port + 1}",
|
|
63
|
-
language: params["language"]))
|
|
69
|
+
language: params["language"], enabled_rules: enabled_rules))
|
|
64
70
|
rescue => e
|
|
65
71
|
write_response(client, status: 500,
|
|
66
72
|
body: JSON.generate({"error" => e.message}))
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# The se daemon: process lifecycle only. The wire protocol lives in
|
|
4
|
-
# lib/simple_english/http.rb, the lint engine in
|
|
4
|
+
# lib/simple_english/daemon/http.rb, the lint engine in
|
|
5
|
+
# lib/simple_english/daemon/engine.rb.
|
|
5
6
|
# Failures raise typed errors (PortInUse, SetupError, InnerDied,
|
|
6
7
|
# InnerTimeout). bin/se owns turning them into warnings and exit codes.
|
|
7
8
|
|
|
@@ -11,7 +12,10 @@ require "socket"
|
|
|
11
12
|
require "tempfile"
|
|
12
13
|
require "tmpdir"
|
|
13
14
|
|
|
14
|
-
require_relative "install"
|
|
15
|
+
require_relative "../setup/install"
|
|
16
|
+
require_relative "../client/daemon"
|
|
17
|
+
require_relative "../setup/fingerprint"
|
|
18
|
+
require_relative "../version"
|
|
15
19
|
|
|
16
20
|
module SimpleEnglish
|
|
17
21
|
module Server
|
|
@@ -40,24 +44,123 @@ module SimpleEnglish
|
|
|
40
44
|
# Probes both loopback families: on IPv6-first resolvers a "localhost"
|
|
41
45
|
# probe falls back to IPv4 and misses an IPv6 listener.
|
|
42
46
|
def assert_port_free(port)
|
|
47
|
+
raise PortInUse, port unless port_free?(port)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def port_free?(port)
|
|
43
51
|
["127.0.0.1", "::1"].each do |address|
|
|
44
52
|
TCPServer.new(address, port).close
|
|
45
|
-
rescue Errno::EADDRNOTAVAIL
|
|
53
|
+
rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT
|
|
54
|
+
# One family may be missing on this host (EADDRNOTAVAIL, or
|
|
55
|
+
# EAFNOSUPPORT when the kernel has no IPv6 at all): skip it
|
|
56
|
+
# and probe the other. A rescue at method scope returns here
|
|
57
|
+
# and never probes the second family.
|
|
46
58
|
next
|
|
47
59
|
end
|
|
60
|
+
true
|
|
48
61
|
rescue Errno::EADDRINUSE
|
|
49
|
-
|
|
62
|
+
false
|
|
50
63
|
rescue Errno::EACCES
|
|
51
64
|
raise ServerError, "port #{port} cannot be bound. Use a port above 1024."
|
|
52
65
|
end
|
|
53
66
|
|
|
67
|
+
# `se serve` over a live daemon (gem upgrade): when
|
|
68
|
+
# one of our daemons holds the port (it answers the
|
|
69
|
+
# handshake), stop it and take over the port. A service that
|
|
70
|
+
# does not answer the handshake, pre-handshake daemon included,
|
|
71
|
+
# keeps the PortInUse error: those need a manual look.
|
|
72
|
+
def takeover(port)
|
|
73
|
+
daemon = Client.info(base_url: "http://localhost:#{port}")
|
|
74
|
+
return unless daemon.is_a?(Hash)
|
|
75
|
+
stop(daemon["pid"])
|
|
76
|
+
# The outer listener closes first. The inner JVM takes seconds
|
|
77
|
+
# longer. Both ports must free, or the next boot hits the inner
|
|
78
|
+
# port and dies.
|
|
79
|
+
deadline = Time.now + 10
|
|
80
|
+
sleep 0.2 until Time.now > deadline ||
|
|
81
|
+
!Client.info(base_url: "http://localhost:#{port}").is_a?(Hash) &&
|
|
82
|
+
port_free?(port + 1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Graceful only. SIGKILL skips the TERM trap and the inner JVM
|
|
86
|
+
# teardown. The orphan still holds port + 1 and breaks the next
|
|
87
|
+
# boot. The pid comes off the wire, so only a positive integer is
|
|
88
|
+
# TERMed: 0 signals the caller's own process group, a negative
|
|
89
|
+
# one every process the user may signal.
|
|
90
|
+
def stop(pid)
|
|
91
|
+
return nil unless pid.is_a?(Integer) && pid > 1
|
|
92
|
+
Process.kill("TERM", pid)
|
|
93
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Everything a boot needs on the machine, checked before the
|
|
98
|
+
# takeover stops the old daemon: a reload that cannot boot must
|
|
99
|
+
# leave the running daemon alive.
|
|
100
|
+
def preflight_install(install)
|
|
101
|
+
return if File.exist?(install.server_jar) && install.java?
|
|
102
|
+
raise Install::SetupError,
|
|
103
|
+
install.java? ? install.setup_error : install.java_message
|
|
104
|
+
end
|
|
105
|
+
private_class_method :preflight_install
|
|
106
|
+
|
|
107
|
+
# `se serve --detached`: replace any se daemon on the port, then
|
|
108
|
+
# boot a plain foreground serve as a detached child - the same
|
|
109
|
+
# shape the auto-boot spawns. The parent returns once the
|
|
110
|
+
# handshake answers. nil means the child never came up, and the
|
|
111
|
+
# visible serve (same command, no flag) shows why. dev_log is
|
|
112
|
+
# forwarded so a detached daemon can still get a log file.
|
|
113
|
+
def start_detached(port: Client::DEFAULT_PORT, install: Install.from_env,
|
|
114
|
+
dev_log: nil)
|
|
115
|
+
preflight_install(install)
|
|
116
|
+
takeover(port)
|
|
117
|
+
# A foreign port holder survives the takeover untouched: fail
|
|
118
|
+
# in milliseconds like the foreground boot, not after the
|
|
119
|
+
# 90 s spawn timeout.
|
|
120
|
+
assert_port_free(port)
|
|
121
|
+
assert_port_free(port + 1)
|
|
122
|
+
args = ["--port", port.to_s]
|
|
123
|
+
args.concat(["--dev-log", dev_log]) if dev_log
|
|
124
|
+
pid = Client.spawn_daemon(*args)
|
|
125
|
+
# The child gives its inner JVM TIMEOUT_SECONDS to boot, and
|
|
126
|
+
# the handshake answers only after that: wait the child's own
|
|
127
|
+
# budget, not the default 90 s, or a slow boot reads as
|
|
128
|
+
# failure while the child is still coming up.
|
|
129
|
+
daemon = Client.wait_for(
|
|
130
|
+
seconds: SimpleEnglish::LanguageTool::TIMEOUT_SECONDS + 10
|
|
131
|
+
) { Client.info(base_url: "http://localhost:#{port}") }
|
|
132
|
+
return daemon if daemon.is_a?(Hash)
|
|
133
|
+
# Readiness failed. The child is either dead (InnerTimeout and
|
|
134
|
+
# InnerDied make it exit itself) or a straggler that later
|
|
135
|
+
# takes over the port from whatever daemon comes next.
|
|
136
|
+
# Stop it either way, and reap so nothing is left behind.
|
|
137
|
+
stop(pid)
|
|
138
|
+
begin
|
|
139
|
+
Process.wait(pid)
|
|
140
|
+
rescue SystemCallError
|
|
141
|
+
# The child already exited on its own (InnerTimeout,
|
|
142
|
+
# InnerDied): only the reap is left.
|
|
143
|
+
end
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
54
147
|
def start(port: Client::DEFAULT_PORT, install: Install.from_env, log: $stderr)
|
|
148
|
+
rules_dir = Dir.mktmpdir("se-rules")
|
|
149
|
+
# The enabled IDs come from the staged file, so what LT loads
|
|
150
|
+
# and what each request enables can never diverge.
|
|
151
|
+
staged = stage_rules(rules_dir)
|
|
152
|
+
enabled_rules = SimpleEnglish::LanguageTool.rule_ids
|
|
153
|
+
daemon_info = {"version" => VERSION, "pid" => Process.pid,
|
|
154
|
+
"gem_digest" => Fingerprint.gem,
|
|
155
|
+
"rules_digest" => Fingerprint.sha(File.read(staged))}
|
|
156
|
+
# Preflight everything the replacement needs before the
|
|
157
|
+
# takeover stops the old daemon.
|
|
158
|
+
preflight_install(install)
|
|
159
|
+
takeover(port)
|
|
55
160
|
assert_port_free(port)
|
|
56
161
|
# The inner JVM lives on port + 1. Guard it too so an occupied
|
|
57
162
|
# inner port raises in milliseconds instead of timing out later.
|
|
58
163
|
assert_port_free(port + 1)
|
|
59
|
-
rules_dir = Dir.mktmpdir("se-rules")
|
|
60
|
-
stage_rules(rules_dir)
|
|
61
164
|
# The inner JVM's stderr goes to a file so failure messages can quote
|
|
62
165
|
# its first line. Only an explicitly opened dev log (a File) is reused
|
|
63
166
|
# for that. $stderr reports path "<STDERR>", so it creates a file
|
|
@@ -103,7 +206,10 @@ module SimpleEnglish
|
|
|
103
206
|
# Select timeout on an idle socket: loop back and select again.
|
|
104
207
|
next if ready.nil?
|
|
105
208
|
client = server.accept
|
|
106
|
-
Thread.new(client) { |c|
|
|
209
|
+
Thread.new(client) { |c|
|
|
210
|
+
HTTP.handle_client(c, port: port,
|
|
211
|
+
enabled_rules: enabled_rules, info: daemon_info)
|
|
212
|
+
}
|
|
107
213
|
rescue IOError, Errno::EBADF
|
|
108
214
|
# A trap or the inner-death monitor closed the listener. This
|
|
109
215
|
# happens during the select, or between select and accept.
|
|
@@ -20,7 +20,12 @@ module SimpleEnglish
|
|
|
20
20
|
".java" => "java",
|
|
21
21
|
".sh" => "bash",
|
|
22
22
|
".kt" => "kotlin",
|
|
23
|
-
".cs" => "csharp"
|
|
23
|
+
".cs" => "csharp",
|
|
24
|
+
".cpp" => "cpp",
|
|
25
|
+
".cc" => "cpp",
|
|
26
|
+
".cxx" => "cpp",
|
|
27
|
+
".hpp" => "cpp",
|
|
28
|
+
".h" => "cpp"
|
|
24
29
|
}.freeze
|
|
25
30
|
|
|
26
31
|
module_function
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# One lint result. Positions are 1-based. The end position is exclusive.
|
|
4
|
+
# Counting findings have no columns or end positions. Findings from an older
|
|
5
|
+
# daemon can have a start column without an end position.
|
|
6
|
+
|
|
7
|
+
module SimpleEnglish
|
|
8
|
+
Finding = Struct.new(:line, :column, :end_line, :end_column, :rule, :message)
|
|
9
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# One decision, two callers: the CLI (SimpleEnglish.lint_file) and
|
|
4
|
+
# the daemon (Engine.lint) must agree on what kind of lint an input
|
|
5
|
+
# is. The CLI decides to skip comment-free files without a boot.
|
|
6
|
+
# The daemon decides again on its side of the wire. Both call this
|
|
7
|
+
# module, so the two tiers cannot drift.
|
|
8
|
+
|
|
9
|
+
module SimpleEnglish
|
|
10
|
+
module LintPlan
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Returns :prose when there is no language (the Markdown
|
|
14
|
+
# pipeline), nil for a comment-free code file (nothing to lint:
|
|
15
|
+
# no boot, no POST), or the comment spans for a code file.
|
|
16
|
+
def call(text, language)
|
|
17
|
+
return :prose unless language
|
|
18
|
+
spans = SimpleEnglish::Extractor.comment_spans(text, language)
|
|
19
|
+
spans.empty? ? nil : spans
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -21,21 +21,39 @@ module SimpleEnglish
|
|
|
21
21
|
module_function
|
|
22
22
|
|
|
23
23
|
# Blank out code and other non-prose blocks and neutralize inline
|
|
24
|
-
# code and heading markers.
|
|
25
|
-
# source.
|
|
24
|
+
# code and heading markers. Line count and UTF-16 widths stay identical
|
|
25
|
+
# to the source so LanguageTool offsets remain source positions.
|
|
26
26
|
def strip(text)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
blank = blank_rows(text).to_h { |row| [row, true] }
|
|
28
|
+
text.lines.each_with_index.map do |line, row|
|
|
29
|
+
ending = if line.end_with?("\r\n")
|
|
30
|
+
"\r\n"
|
|
31
|
+
elsif line.end_with?("\n")
|
|
32
|
+
"\n"
|
|
33
|
+
else
|
|
34
|
+
""
|
|
35
|
+
end
|
|
36
|
+
body = line.delete_suffix(ending)
|
|
37
|
+
(blank[row] ? spaces_for(body) : strip_line(body)) + ending
|
|
38
|
+
end.join
|
|
30
39
|
end
|
|
31
40
|
|
|
32
41
|
def strip_line(line)
|
|
33
|
-
# Width-preserving replacements
|
|
34
|
-
# the source
|
|
35
|
-
line
|
|
42
|
+
# Width-preserving replacements keep LanguageTool positions aligned
|
|
43
|
+
# with the source.
|
|
44
|
+
line
|
|
45
|
+
.gsub(/`[^`]*`/) { |code| "X" * utf16_length(code) }
|
|
36
46
|
.sub(/\A\#{1,6} /) { |marker| " " * marker.length }
|
|
37
47
|
end
|
|
38
48
|
|
|
49
|
+
def spaces_for(text)
|
|
50
|
+
" " * utf16_length(text)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def utf16_length(text)
|
|
54
|
+
text.each_char.sum { |char| (char.ord > 0xFFFF) ? 2 : 1 }
|
|
55
|
+
end
|
|
56
|
+
|
|
39
57
|
# A vertical list is not one paragraph: each list item is its own.
|
|
40
58
|
# Paragraph boundaries come from the tree-sitter Markdown grammar,
|
|
41
59
|
# so lazy continuations and interrupted lists match the spec
|
|
@@ -97,7 +115,7 @@ module SimpleEnglish
|
|
|
97
115
|
TreeSitterLanguagePack.get_parser("markdown").parse(text).root_node
|
|
98
116
|
end
|
|
99
117
|
|
|
100
|
-
private_class_method :strip_line, :
|
|
101
|
-
:inside_list_item?, :parse
|
|
118
|
+
private_class_method :strip_line, :spaces_for, :utf16_length, :node_rows,
|
|
119
|
+
:blank_rows, :each_node, :inside_list_item?, :parse
|
|
102
120
|
end
|
|
103
121
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleEnglish
|
|
4
|
+
# The plain-text payload for /v2/check, duck-compatible with
|
|
5
|
+
# AnnotatedText::Result: #lt_params and #locate.
|
|
6
|
+
PlainText = Struct.new(:text) do
|
|
7
|
+
def lt_params
|
|
8
|
+
{"text" => text}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# LanguageTool reports offsets in Java UTF-16 code units.
|
|
12
|
+
# Return a 1-based line and UTF-16 column for a 0-based offset.
|
|
13
|
+
def locate(utf16_offset)
|
|
14
|
+
line = 1
|
|
15
|
+
column = 1
|
|
16
|
+
units = 0
|
|
17
|
+
text.each_char do |char|
|
|
18
|
+
return [line, column] if units >= utf16_offset
|
|
19
|
+
units += (char.ord > 0xFFFF) ? 2 : 1
|
|
20
|
+
if char == "\n"
|
|
21
|
+
line += 1
|
|
22
|
+
column = 1
|
|
23
|
+
else
|
|
24
|
+
column += (char.ord > 0xFFFF) ? 2 : 1
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
[line, column]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SimpleEnglish
|
|
4
4
|
module AnnotatedText
|
|
5
5
|
# The payload for code comments. Duck interface shared with
|
|
6
|
-
#
|
|
6
|
+
# PlainText: #lt_params (the form data for LanguageTool)
|
|
7
7
|
# and #locate (a match offset back to file line and column).
|
|
8
8
|
Result = Struct.new(:source, :segments, :stream) do
|
|
9
9
|
def lt_params
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# .simple-english.yml: ignore (path globs, matched against the paths
|
|
4
|
-
# on the command line) and disabled-rules
|
|
3
|
+
# .simple-english.yml: ignore (path globs, matched against the paths
|
|
4
|
+
# given on the command line) and disabled-rules (rule IDs dropped
|
|
5
|
+
# from every file). Loaded from the CWD.
|
|
5
6
|
|
|
6
7
|
module SimpleEnglish
|
|
7
8
|
module Config
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fingerprints for the daemon handshake. `gem` identifies the code and
|
|
4
|
+
# built-in rules a daemon runs: any release that changes lint behavior
|
|
5
|
+
# changes it, and nothing else does. `sha` covers the staged rule
|
|
6
|
+
# content wherever the caller resolved it.
|
|
7
|
+
|
|
8
|
+
require_relative "languagetool"
|
|
9
|
+
|
|
10
|
+
module SimpleEnglish
|
|
11
|
+
module Fingerprint
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Hash everything the daemon can execute, not a curated list: a
|
|
15
|
+
# curated list goes stale the first time a new file is forgotten,
|
|
16
|
+
# and a missed file means silent staleness, the exact bug the
|
|
17
|
+
# handshake exists to catch. The accepted cost: a release that
|
|
18
|
+
# only changes caller-side code triggers one needless restart.
|
|
19
|
+
# ponytail: whole-lib glob. If restarts ever hurt, curate per-file.
|
|
20
|
+
def gem
|
|
21
|
+
files = Dir.glob(File.expand_path("../../**/*.rb", __dir__)).sort
|
|
22
|
+
files << LanguageTool::RULES_FILE
|
|
23
|
+
# Each file is length-prefixed: bare concatenation cannot tell
|
|
24
|
+
# ["ab", "c"] from ["a", "bc"], so bytes moved across a file
|
|
25
|
+
# boundary leave the digest unchanged.
|
|
26
|
+
framed = files.map do |path|
|
|
27
|
+
content = File.read(path)
|
|
28
|
+
"#{content.bytesize}\n#{content}"
|
|
29
|
+
end
|
|
30
|
+
sha(framed.join)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sha(content)
|
|
34
|
+
require "digest"
|
|
35
|
+
::Digest::SHA256.hexdigest(content)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -13,11 +13,12 @@ module SimpleEnglish
|
|
|
13
13
|
class SetupError < StandardError
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
attr_reader :cache_dir, :java
|
|
16
|
+
attr_reader :cache_dir, :java, :java_source
|
|
17
17
|
|
|
18
|
-
def initialize(cache_dir:, java: nil)
|
|
18
|
+
def initialize(cache_dir:, java: nil, java_source: nil)
|
|
19
19
|
@cache_dir = cache_dir
|
|
20
20
|
@java = java
|
|
21
|
+
@java_source = java_source
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# The one place the environment is read. SE_JAVA wins outright,
|
|
@@ -25,15 +26,21 @@ module SimpleEnglish
|
|
|
25
26
|
# Otherwise the code probes the PATH candidate by running it,
|
|
26
27
|
# because a file can exist and still be the macOS stub that
|
|
27
28
|
# reports no runtime. The last resort is the Homebrew location,
|
|
28
|
-
# which sits outside PATH.
|
|
29
|
+
# which sits outside PATH. java_source records which of the three
|
|
30
|
+
# won, so `se setup` can say so.
|
|
29
31
|
def self.from_env(env = ENV)
|
|
30
32
|
cache = File.expand_path(env.fetch(LanguageTool::CACHE_DIR_ENV) do
|
|
31
33
|
File.join(Dir.home, ".cache", "se")
|
|
32
34
|
end)
|
|
33
|
-
java =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
java, source =
|
|
36
|
+
if (explicit = env[LanguageTool::JAVA_ENV])
|
|
37
|
+
[explicit, :env]
|
|
38
|
+
elsif probe?("java", env)
|
|
39
|
+
["java", :path]
|
|
40
|
+
elsif File.executable?(LanguageTool::HOMEBREW_JAVA)
|
|
41
|
+
[LanguageTool::HOMEBREW_JAVA, :homebrew]
|
|
42
|
+
end
|
|
43
|
+
new(cache_dir: cache, java: java, java_source: source)
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
# Runs the candidate under the given PATH, isolated from this
|
|
@@ -50,8 +57,11 @@ module SimpleEnglish
|
|
|
50
57
|
|
|
51
58
|
def java!
|
|
52
59
|
return java if java?
|
|
53
|
-
raise SetupError,
|
|
54
|
-
|
|
60
|
+
raise SetupError, java_message
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def java_message
|
|
64
|
+
"java not found. Install a JRE (on macOS: brew install openjdk), " \
|
|
55
65
|
"or set SE_JAVA to your java binary."
|
|
56
66
|
end
|
|
57
67
|
|