laneyard 0.5.0 → 0.6.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.
- package/README.md +274 -305
- package/dist/src/cli/adopt.js +221 -0
- package/dist/src/cli/remove.js +47 -25
- package/dist/src/cli/setup.js +30 -0
- package/dist/src/config/schema.js +10 -4
- package/dist/src/fastfile/adoption.js +142 -0
- package/dist/src/fastfile/splice.js +38 -0
- package/dist/src/main.js +4 -18
- package/dist/src/sidecar/bridge.js +5 -4
- package/dist/src/sidecar/prism-ruby.js +42 -0
- package/dist/src/sidecar/scan.js +44 -0
- package/dist/web/assets/{Editor-Bqz_dClT.js → Editor-CMa4-4N3.js} +1 -1
- package/dist/web/assets/{index-CMEreqtd.js → index-B8lAQPEM.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/ruby/scan.rb +176 -0
package/ruby/scan.rb
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Laneyard's Fastfile scanner.
|
|
5
|
+
#
|
|
6
|
+
# ruby scan.rb --fastlane-dir fastlane
|
|
7
|
+
#
|
|
8
|
+
# Deliberately ignorant. It reports the keyword arguments given directly to a
|
|
9
|
+
# call whose value is a literal string, with the byte ranges of the value and of
|
|
10
|
+
# the whole `key: value` pair, and has no idea what a credential is. Deciding
|
|
11
|
+
# which of those matters is `src/fastfile/adoption.ts`'s job, next to the one
|
|
12
|
+
# table that already describes each credential kind — a second copy of that
|
|
13
|
+
# table here would be free to disagree with it, in a language that cannot check
|
|
14
|
+
# it.
|
|
15
|
+
#
|
|
16
|
+
# Nested structures are deliberately not descended into. The literal inside
|
|
17
|
+
# `gym(export_options: { provisioningProfiles: { "id" => "./x" } })` has no
|
|
18
|
+
# keyword to report — its key is a bundle id — and attributing it to `gym` would
|
|
19
|
+
# be noise the caller cannot act on. This claim stays narrow on purpose: a
|
|
20
|
+
# confident answer that is wrong is worse than one that admits its edge.
|
|
21
|
+
#
|
|
22
|
+
# It never requires fastlane. `introspect.rb` must, to enumerate lanes; a
|
|
23
|
+
# syntax tree needs only Prism, and paying a fastlane boot for it would make
|
|
24
|
+
# this too slow to run during `laneyard setup`.
|
|
25
|
+
#
|
|
26
|
+
# The output contract is `introspect.rb`'s: { "ok": true, ... } or
|
|
27
|
+
# { "ok": false, "error": "..." }. An error is a valid response.
|
|
28
|
+
|
|
29
|
+
require "json"
|
|
30
|
+
|
|
31
|
+
REAL_STDOUT = $stdout.dup
|
|
32
|
+
|
|
33
|
+
# Writes the one JSON object this process produces, and stops.
|
|
34
|
+
#
|
|
35
|
+
# Serialisation is *not* done here, on purpose: `JSON.generate` raises on a
|
|
36
|
+
# string that is not valid UTF-8, and a Fastfile may well hold one
|
|
37
|
+
# (`supply(json_key: "./key\xFF.json")` is enough). Raising from inside the
|
|
38
|
+
# writer put the failure outside every guard in this file, so the caller got a
|
|
39
|
+
# Ruby trace on stderr and nothing at all on stdout — the one outcome the
|
|
40
|
+
# contract at the top promises never to happen. Callers generate under a guard
|
|
41
|
+
# and hand the finished string here.
|
|
42
|
+
def emit(json)
|
|
43
|
+
REAL_STDOUT.puts json
|
|
44
|
+
REAL_STDOUT.flush
|
|
45
|
+
exit 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def respond(payload)
|
|
49
|
+
emit(JSON.generate(payload))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fail_with(message)
|
|
53
|
+
# Scrubbed, because an error message can quote the bytes that caused it:
|
|
54
|
+
# `JSON::GeneratorError` names the offending literal. An error path that
|
|
55
|
+
# raises while reporting an error leaves the caller with nothing.
|
|
56
|
+
respond({ ok: false, error: message.to_s.dup.force_encoding("UTF-8").scrub("?") })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
dir_index = ARGV.index("--fastlane-dir")
|
|
60
|
+
fastlane_dir = dir_index ? ARGV[dir_index + 1] : "fastlane"
|
|
61
|
+
fastfile_path = File.join(Dir.pwd, fastlane_dir, "Fastfile")
|
|
62
|
+
|
|
63
|
+
fail_with("Fastfile not found: #{fastfile_path}") unless File.exist?(fastfile_path)
|
|
64
|
+
|
|
65
|
+
# Insurance, not a fix for a known culprit: unlike `introspect.rb`, which faces
|
|
66
|
+
# a fastlane that really does print banners and deprecation notices, nothing
|
|
67
|
+
# here prints — Prism parses silently and the Fastfile is never executed. The
|
|
68
|
+
# redirection stands so that anything which *starts* printing later (a stray
|
|
69
|
+
# `puts`, a warning from some future require) lands on stderr instead of
|
|
70
|
+
# corrupting the one JSON object the real output carries.
|
|
71
|
+
$stdout = $stderr
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
require "prism"
|
|
75
|
+
rescue LoadError => e
|
|
76
|
+
fail_with("prism is not available in this Ruby (#{e.message})")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Every `key: "literal"` inside a call, wherever the call sits.
|
|
80
|
+
#
|
|
81
|
+
# Descends through everything: a call inside an `if`, inside a `def`, inside a
|
|
82
|
+
# `platform` block, is still a call this file makes. `calls_within` in
|
|
83
|
+
# introspect.rb resolves helper methods to attribute actions to a *lane*; there
|
|
84
|
+
# is no such need here, because a literal path is a problem wherever it is
|
|
85
|
+
# written and belongs to the file rather than to any one lane.
|
|
86
|
+
def literals_in(node, out = [])
|
|
87
|
+
return out if node.nil?
|
|
88
|
+
|
|
89
|
+
node.compact_child_nodes.each do |child|
|
|
90
|
+
if child.is_a?(Prism::CallNode)
|
|
91
|
+
# Both node types, because `supply(json_key: "…")` parses as a
|
|
92
|
+
# KeywordHashNode and `supply({json_key: "…"})` as a HashNode. They are
|
|
93
|
+
# the same argument written two ways, and looking for only the first
|
|
94
|
+
# missed the braced form entirely — silently, since a scan that finds
|
|
95
|
+
# nothing is indistinguishable from a file with nothing to find.
|
|
96
|
+
hashes = (child.arguments&.arguments || []).select do |a|
|
|
97
|
+
a.is_a?(Prism::KeywordHashNode) || a.is_a?(Prism::HashNode)
|
|
98
|
+
end
|
|
99
|
+
hashes.flat_map(&:elements).each do |el|
|
|
100
|
+
next unless el.is_a?(Prism::AssocNode)
|
|
101
|
+
next unless el.key.is_a?(Prism::SymbolNode)
|
|
102
|
+
next unless el.value.is_a?(Prism::StringNode)
|
|
103
|
+
# A heredoc's location covers its *marker* — `<<~P` is four bytes —
|
|
104
|
+
# while its value lives on the lines below. Reporting that range would
|
|
105
|
+
# have the caller splice `ENV.fetch("...")` over the marker and leave
|
|
106
|
+
# the body stranded after the call: a Fastfile that no longer parses,
|
|
107
|
+
# written silently into someone's repository. Dropping heredocs costs
|
|
108
|
+
# nothing real, because a credential path is never written as one while
|
|
109
|
+
# `changelog:` and `message:` routinely are. Only Ruby can tell a
|
|
110
|
+
# heredoc from a quoted string, so the decision has to be made here.
|
|
111
|
+
next if el.value.opening&.start_with?("<<")
|
|
112
|
+
|
|
113
|
+
# `start_offset` and `length` are byte offsets, and must stay that way:
|
|
114
|
+
# the caller splices them into a Buffer. Prism also offers
|
|
115
|
+
# `start_character_offset`, and swapping one in would still pass any
|
|
116
|
+
# test whose fixture is pure ASCII while putting one accent above the
|
|
117
|
+
# literal enough to land a patch mid-string in a build file.
|
|
118
|
+
out << {
|
|
119
|
+
action: child.name.to_s,
|
|
120
|
+
arg: el.key.unescaped,
|
|
121
|
+
value: el.value.unescaped,
|
|
122
|
+
value_start: el.value.location.start_offset,
|
|
123
|
+
value_length: el.value.location.length,
|
|
124
|
+
pair_start: el.location.start_offset,
|
|
125
|
+
pair_length: el.location.length,
|
|
126
|
+
line: el.location.start_line
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
literals_in(child, out)
|
|
131
|
+
end
|
|
132
|
+
out
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A Fastfile that does not parse, told apart from everything else that can go
|
|
136
|
+
# wrong in the block below, so each gets a sentence that is true of it.
|
|
137
|
+
ParseFailure = Class.new(StandardError)
|
|
138
|
+
|
|
139
|
+
# Prism's first complaint, and the line it points at.
|
|
140
|
+
#
|
|
141
|
+
# "Fastfile could not be parsed" on its own leaves someone opening the file and
|
|
142
|
+
# hunting; a flow that is about to offer to *edit* that file owes them better
|
|
143
|
+
# than that. The first error is the one worth showing — the ones behind it are
|
|
144
|
+
# usually the same mistake seen again from further down.
|
|
145
|
+
def parse_failure(result)
|
|
146
|
+
first = result.errors.first
|
|
147
|
+
return "Fastfile could not be parsed" unless first
|
|
148
|
+
|
|
149
|
+
"Fastfile could not be parsed, line #{first.location.start_line}: #{first.message}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
begin
|
|
153
|
+
source = File.read(fastfile_path)
|
|
154
|
+
result = Prism.parse(source)
|
|
155
|
+
# A plain `raise`, not `fail_with`, and deliberately so: `fail_with` ends in
|
|
156
|
+
# `respond`'s `exit 0`, which raises SystemExit — a subclass of Exception, so
|
|
157
|
+
# calling it from inside this very `begin` would be caught by the `rescue`
|
|
158
|
+
# below and answered a second time, corrupting the output with two JSON
|
|
159
|
+
# blobs. Raising here and answering from the `rescue` clause (which is not
|
|
160
|
+
# itself guarded) is the same shape `introspect.rb` uses for this.
|
|
161
|
+
raise ParseFailure, parse_failure(result) unless result.success?
|
|
162
|
+
# Serialised here rather than at the `emit` below, so that a literal Prism
|
|
163
|
+
# read fine but JSON cannot represent — anything that is not valid UTF-8 —
|
|
164
|
+
# becomes an ordinary `{ "ok": false }` instead of a trace. Only the write
|
|
165
|
+
# stays outside the guard, for the SystemExit reason above.
|
|
166
|
+
payload = JSON.generate({ ok: true, literals: literals_in(result.value) })
|
|
167
|
+
rescue ParseFailure => e
|
|
168
|
+
# Already a whole sentence, and prefixing it produced the visible doubling
|
|
169
|
+
# this replaced: "Could not read the Fastfile: Fastfile could not be parsed",
|
|
170
|
+
# whose first half was also false — the read had succeeded.
|
|
171
|
+
fail_with(e.message)
|
|
172
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
173
|
+
fail_with("Could not scan the Fastfile: #{e.message}")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
emit(payload)
|