carton-tools 0.3.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 +3 -3
- data/lib/carton/tools/bindings.rb +51 -11
- data/lib/carton/tools/checks.rb +87 -53
- data/lib/carton/tools/cli.rb +45 -8
- data/lib/carton/tools/project.rb +31 -31
- data/lib/carton/tools/signatures.rb +18 -120
- data/lib/carton/tools/source_file.rb +24 -55
- data/lib/carton/tools/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ae3c3331c46059824d9cfee8857d5db202c219b1559af6d9d8ef9f5c97ebc37
|
|
4
|
+
data.tar.gz: 3ea2079bd6ce6e8796bbe0d6285dfcccd910c89f47392eccabf5d31d13e3b053
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2015b0648a2f087ea87fa9a8aa930afad24b36aa3ff5d4f1058ef6bde8d55fc95eee356d7476365177ed5ffa453f35043398d91a19b6a6789aa7d1331b3804a3
|
|
7
|
+
data.tar.gz: ad3864182e38083ed977bc231c9d8db3882c7e8289138f57036d06f62a5975880231a30ae2621965e5aabe7d9bc158e0eeed0549d0ceb2a61e5be43bf39d7a64
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# carton-tools
|
|
2
2
|
|
|
3
|
-
The `carton` command for [Carton](../README.md). It reads code with Prism and never runs it, so it needs no `RUBY_BOX=1`.
|
|
3
|
+
The `carton` command for [Carton](../README.md) apps with explicit imports and exports. It reads code with Prism and never runs it, so it needs no `RUBY_BOX=1`.
|
|
4
4
|
|
|
5
|
-
- `carton check main.rb` reports import and export problems: missing imports, cycles, `require` calls that reach into a Carton, missing keys of an exported Hash, and gems a
|
|
5
|
+
- `carton check main.rb` reports import and export problems: missing imports, cycles, `require` calls that reach into a Carton, missing keys of an exported Hash, and gems a Gem Carton uses without declaring them.
|
|
6
6
|
- `carton rbs main.rb` writes `sig/generated/carton.rbs`, so [Steep](https://github.com/soutaro/steep) knows what each import returns.
|
|
7
7
|
|
|
8
8
|
Add it next to `carton`, in the group your checks run in:
|
|
@@ -17,6 +17,6 @@ gem 'carton-tools', group: :development
|
|
|
17
17
|
bundle exec carton check main.rb
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
carton-tools is released with carton and depends on exactly the same version. See [Types and checks](
|
|
20
|
+
carton-tools is released with carton and depends on exactly the same version. See [Types and checks](https://carton.dtcristo.com/guides/types-and-checks/) for what each command does.
|
|
21
21
|
|
|
22
22
|
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -10,7 +10,7 @@ module Carton
|
|
|
10
10
|
# a local binding only in its own file, and only when nothing reassigns it.
|
|
11
11
|
class Bindings
|
|
12
12
|
# A name bound to an imported Carton, with each Use of it.
|
|
13
|
-
Binding = Data.define(:name, :
|
|
13
|
+
Binding = Data.define(:name, :local, :carton, :file, :line, :uses)
|
|
14
14
|
|
|
15
15
|
# One Reference to a binding, and the file it is in.
|
|
16
16
|
Use = Data.define(:file, :reference)
|
|
@@ -21,15 +21,32 @@ module Carton
|
|
|
21
21
|
@project = project
|
|
22
22
|
@by_key = {}
|
|
23
23
|
@all = []
|
|
24
|
+
@defined = Hash.new { |hash, box| hash[box] = {} }
|
|
25
|
+
@project.files.each_value do |file|
|
|
26
|
+
(file.definitions.keys + file.constants.keys).each do |name|
|
|
27
|
+
@defined[box_of(file.path)][name] = true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
24
30
|
bind_imports
|
|
25
31
|
collect_uses
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
# Imports matched directly against a pattern, `import('math') => { add: }`,
|
|
29
|
-
# as [call,
|
|
35
|
+
# as [call, carton] pairs.
|
|
30
36
|
def patterns
|
|
31
37
|
@project.edges.filter_map do |edge|
|
|
32
|
-
[edge.call, edge.
|
|
38
|
+
[edge.call, edge.carton] if edge.carton && edge.call.pattern
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Definitions that open an imported value or a constant beneath it,
|
|
43
|
+
# as [path, definition, binding] triples. A definition is a use, too.
|
|
44
|
+
def reopenings
|
|
45
|
+
@project.files.each_value.flat_map do |file|
|
|
46
|
+
file.definition_sites.filter_map do |site|
|
|
47
|
+
binding = reopened_binding(file.path, site)
|
|
48
|
+
[file.path, site, binding] if binding
|
|
49
|
+
end
|
|
33
50
|
end
|
|
34
51
|
end
|
|
35
52
|
|
|
@@ -39,18 +56,19 @@ module Carton
|
|
|
39
56
|
clashing = {}
|
|
40
57
|
@project.edges.each do |edge|
|
|
41
58
|
call = edge.call
|
|
42
|
-
next unless edge.
|
|
59
|
+
next unless edge.carton
|
|
43
60
|
|
|
44
61
|
key = binding_key(call)
|
|
45
62
|
next unless key
|
|
46
63
|
|
|
47
64
|
existing = @by_key[key]
|
|
48
|
-
clashing[key] = true if existing && existing.
|
|
65
|
+
clashing[key] = true if existing && existing.carton != edge.carton
|
|
49
66
|
next if existing
|
|
50
67
|
|
|
51
68
|
@by_key[key] = Binding.new(
|
|
52
69
|
name: call.constant || call.local,
|
|
53
|
-
|
|
70
|
+
local: !call.local.nil?,
|
|
71
|
+
carton: edge.carton,
|
|
54
72
|
file: call.file,
|
|
55
73
|
line: call.line,
|
|
56
74
|
uses: [],
|
|
@@ -83,25 +101,47 @@ module Carton
|
|
|
83
101
|
def lookup(path, reference)
|
|
84
102
|
return @by_key[[:local, path, reference.name]] if reference.local
|
|
85
103
|
|
|
104
|
+
constant(path, reference.name, reference.scope)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def constant(path, name, scope)
|
|
86
108
|
box = box_of(path)
|
|
87
|
-
candidates(
|
|
88
|
-
binding = @by_key[[:constant, box,
|
|
109
|
+
candidates(name, scope).each do |candidate|
|
|
110
|
+
binding = @by_key[[:constant, box, candidate]]
|
|
89
111
|
return binding if binding
|
|
112
|
+
return nil if @defined[box][candidate]
|
|
90
113
|
end
|
|
91
114
|
nil
|
|
92
115
|
end
|
|
93
116
|
|
|
94
|
-
def candidates(
|
|
95
|
-
name = reference.name
|
|
117
|
+
def candidates(name, scope)
|
|
96
118
|
return [name.delete_prefix('::')] if name.start_with?('::')
|
|
97
119
|
|
|
98
|
-
parts =
|
|
120
|
+
parts = scope.to_s.split('::')
|
|
99
121
|
[
|
|
100
122
|
*parts.size.downto(1).map { |n| [*parts.first(n), name].join('::') },
|
|
101
123
|
name,
|
|
102
124
|
]
|
|
103
125
|
end
|
|
104
126
|
|
|
127
|
+
def reopened_binding(path, site)
|
|
128
|
+
# `module Local; class Billing` defines Local::Billing, even if an
|
|
129
|
+
# outer scope imported Billing. Qualified parents resolve lexically.
|
|
130
|
+
unless site.name.include?('::')
|
|
131
|
+
name = [site.scope, site.name].compact.join('::')
|
|
132
|
+
return @by_key[[:constant, box_of(path), name]]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
parts = site.name.split('::')
|
|
136
|
+
parts
|
|
137
|
+
.size
|
|
138
|
+
.downto(1) do |size|
|
|
139
|
+
binding = constant(path, parts.first(size).join('::'), site.scope)
|
|
140
|
+
return binding if binding
|
|
141
|
+
end
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
105
145
|
# Files loaded in the same Box share constants: the app's, or one
|
|
106
146
|
# Carton's.
|
|
107
147
|
def box_of(path) = @project.owner(path)&.entrypoint
|
data/lib/carton/tools/checks.rb
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'bindings'
|
|
4
|
-
require 'carton/namespace_methods'
|
|
5
|
-
require 'carton/namespace'
|
|
6
4
|
require_relative 'project'
|
|
7
5
|
|
|
8
6
|
module Carton
|
|
@@ -19,6 +17,7 @@ module Carton
|
|
|
19
17
|
def initialize(project)
|
|
20
18
|
@project = project
|
|
21
19
|
@bindings = Bindings.new(project)
|
|
20
|
+
@reopenings = @bindings.reopenings
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
def problems
|
|
@@ -29,9 +28,11 @@ module Carton
|
|
|
29
28
|
*requires_into_cartons,
|
|
30
29
|
*undeclared_gems,
|
|
31
30
|
*export_problems,
|
|
32
|
-
*
|
|
31
|
+
*missing_export_declarations,
|
|
33
32
|
*missing_exports,
|
|
34
33
|
*private_constant_references,
|
|
34
|
+
*unused_imports,
|
|
35
|
+
*reopened_classes,
|
|
35
36
|
].sort_by { |problem| [problem.file, problem.line, problem.message] }
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -78,11 +79,11 @@ module Carton
|
|
|
78
79
|
graph = Hash.new { |hash, key| hash[key] = [] }
|
|
79
80
|
@project.edges.each do |edge|
|
|
80
81
|
from = @project.owner(edge.call.file)
|
|
81
|
-
graph[from.entrypoint] << edge if from && edge.
|
|
82
|
+
graph[from.entrypoint] << edge if from && edge.carton
|
|
82
83
|
end
|
|
83
84
|
find_cycles(graph).map do |cycle|
|
|
84
85
|
first = cycle.first.call
|
|
85
|
-
names = [cycle.last.
|
|
86
|
+
names = [cycle.last.carton, *cycle.map(&:carton)].map(&:name)
|
|
86
87
|
message = "import cycle: #{names.join(' -> ')}"
|
|
87
88
|
if cycle.any? { |edge| edge.call.lazy }
|
|
88
89
|
warning(
|
|
@@ -105,15 +106,16 @@ module Carton
|
|
|
105
106
|
end
|
|
106
107
|
cycles
|
|
107
108
|
.sort_by { |cycle| cycle.count { |edge| edge.call.lazy } }
|
|
108
|
-
.uniq { |cycle| cycle.map { |edge| edge.
|
|
109
|
+
.uniq { |cycle| cycle.map { |edge| edge.carton.entrypoint } }
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
def walk(graph, start, node, path, cycles)
|
|
112
113
|
graph[node].each do |edge|
|
|
113
|
-
target = edge.
|
|
114
|
+
target = edge.carton.entrypoint
|
|
114
115
|
if target == start
|
|
115
116
|
cycles << [*path, edge]
|
|
116
|
-
elsif target > start &&
|
|
117
|
+
elsif target > start &&
|
|
118
|
+
path.none? { |e| e.carton.entrypoint == target }
|
|
117
119
|
walk(graph, start, target, [*path, edge], cycles)
|
|
118
120
|
end
|
|
119
121
|
end
|
|
@@ -124,28 +126,28 @@ module Carton
|
|
|
124
126
|
def requires_into_cartons
|
|
125
127
|
@project.requires.filter_map do |call|
|
|
126
128
|
path = @project.locate(call) or next
|
|
127
|
-
|
|
128
|
-
next if
|
|
129
|
+
carton = @project.carton_containing(path) or next
|
|
130
|
+
next if carton == @project.owner(call.file)
|
|
129
131
|
|
|
130
132
|
error(
|
|
131
133
|
call.file,
|
|
132
134
|
call.line,
|
|
133
|
-
"#{call.kind} '#{call.path}' loads a second copy of the #{
|
|
134
|
-
"Carton's #{path ==
|
|
135
|
+
"#{call.kind} '#{call.path}' loads a second copy of the #{carton.name} " \
|
|
136
|
+
"Carton's #{path == carton.entrypoint ? 'Entrypoint' : 'file'}: import it instead",
|
|
135
137
|
)
|
|
136
138
|
end
|
|
137
139
|
end
|
|
138
140
|
|
|
139
|
-
# A
|
|
141
|
+
# A Gem Carton's gemspec is its manifest: every other gem it imports or
|
|
140
142
|
# requires must be a runtime dependency there. Under a bundled app it
|
|
141
143
|
# still loads today, since the Carton sees the app's whole lockfile, but
|
|
142
144
|
# it fails wherever the gem is installed without that dependency. Default
|
|
143
145
|
# gems and Carton itself load in every Box.
|
|
144
146
|
def undeclared_gems
|
|
145
|
-
@project.
|
|
146
|
-
spec = @project.gem_for(
|
|
147
|
+
@project.cartons.each_value.flat_map do |carton|
|
|
148
|
+
spec = @project.gem_for(carton.entrypoint) or next []
|
|
147
149
|
declared = [spec.name, *spec.runtime_dependencies.map(&:name)]
|
|
148
|
-
calls =
|
|
150
|
+
calls = carton.files.flat_map { |path| loads(path) }
|
|
149
151
|
calls.filter_map do |call|
|
|
150
152
|
path = @project.target(call) or next
|
|
151
153
|
used = @project.gem_for(path) or next
|
|
@@ -212,30 +214,16 @@ module Carton
|
|
|
212
214
|
end
|
|
213
215
|
end
|
|
214
216
|
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
def
|
|
218
|
-
@project.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
unit.files.flat_map do |path|
|
|
222
|
-
@project
|
|
223
|
-
.files
|
|
224
|
-
.fetch(path)
|
|
225
|
-
.methods
|
|
226
|
-
.each_value
|
|
227
|
-
.filter_map do |method|
|
|
228
|
-
next unless Namespace.reserved?(method.name)
|
|
229
|
-
|
|
230
|
-
error(path, method.line, collision(method.name))
|
|
231
|
-
end
|
|
217
|
+
# Only imported Entrypoints must export. The presence of a call is
|
|
218
|
+
# checked without proving that every path through the file reaches it.
|
|
219
|
+
def missing_export_declarations
|
|
220
|
+
@project.cartons.each_value.filter_map do |carton|
|
|
221
|
+
unless @project.files.fetch(carton.entrypoint).export_lines.empty?
|
|
222
|
+
next
|
|
232
223
|
end
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
224
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
"Carton::Namespace##{name}: rename it, or export a module"
|
|
225
|
+
error(carton.entrypoint, 1, 'an imported Entrypoint must call export')
|
|
226
|
+
end
|
|
239
227
|
end
|
|
240
228
|
|
|
241
229
|
# A `fetch` of a key a Carton's exported Hash does not have, which
|
|
@@ -245,7 +233,7 @@ module Carton
|
|
|
245
233
|
def missing_exports
|
|
246
234
|
from_bindings =
|
|
247
235
|
@bindings.all.flat_map do |binding|
|
|
248
|
-
names = hash_keys(binding.
|
|
236
|
+
names = hash_keys(binding.carton) or next []
|
|
249
237
|
binding.uses.filter_map do |use|
|
|
250
238
|
reference = use.reference
|
|
251
239
|
next unless raises?(reference)
|
|
@@ -257,7 +245,7 @@ module Carton
|
|
|
257
245
|
missing_message(
|
|
258
246
|
"#{reference.name}.fetch(:#{reference.member})",
|
|
259
247
|
reference.member,
|
|
260
|
-
binding.
|
|
248
|
+
binding.carton,
|
|
261
249
|
names,
|
|
262
250
|
),
|
|
263
251
|
)
|
|
@@ -267,8 +255,8 @@ module Carton
|
|
|
267
255
|
end
|
|
268
256
|
|
|
269
257
|
def missing_pattern_keys
|
|
270
|
-
@bindings.patterns.flat_map do |call,
|
|
271
|
-
names = hash_keys(
|
|
258
|
+
@bindings.patterns.flat_map do |call, carton|
|
|
259
|
+
names = hash_keys(carton)
|
|
272
260
|
next [] unless names && call.pattern.kind == :fetch
|
|
273
261
|
|
|
274
262
|
(call.pattern.keys - ['**'] - names).map do |key|
|
|
@@ -278,7 +266,7 @@ module Carton
|
|
|
278
266
|
missing_message(
|
|
279
267
|
"#{call.kind}('#{call.path}') => { #{key}: }",
|
|
280
268
|
key,
|
|
281
|
-
|
|
269
|
+
carton,
|
|
282
270
|
names,
|
|
283
271
|
),
|
|
284
272
|
)
|
|
@@ -305,15 +293,15 @@ module Carton
|
|
|
305
293
|
caught.intersect?([*raised, 'StandardError', 'Exception'])
|
|
306
294
|
end
|
|
307
295
|
|
|
308
|
-
def missing_message(code, name,
|
|
309
|
-
"#{code}: the #{
|
|
296
|
+
def missing_message(code, name, carton, names)
|
|
297
|
+
"#{code}: the #{carton.name} Carton's export has no key #{name} " \
|
|
310
298
|
"(it has #{names.join(', ')})"
|
|
311
299
|
end
|
|
312
300
|
|
|
313
301
|
# The keys of the Hash a Carton exports, or nil when it exports
|
|
314
302
|
# something else or its keys cannot be read statically.
|
|
315
|
-
def hash_keys(
|
|
316
|
-
exports = @project.files.fetch(
|
|
303
|
+
def hash_keys(carton)
|
|
304
|
+
exports = @project.files.fetch(carton.entrypoint).exports
|
|
317
305
|
exports.first.keys&.keys if exports.size == 1
|
|
318
306
|
end
|
|
319
307
|
|
|
@@ -321,33 +309,79 @@ module Carton
|
|
|
321
309
|
# `Billing` makes `Tax` private, raises NameError.
|
|
322
310
|
def private_constant_references
|
|
323
311
|
@bindings.all.flat_map do |binding|
|
|
324
|
-
|
|
312
|
+
constant = exported_constant(binding.carton) or next []
|
|
325
313
|
privates =
|
|
326
|
-
binding.
|
|
314
|
+
binding.carton.files.flat_map do |path|
|
|
327
315
|
@project.files.fetch(path).private_constants
|
|
328
316
|
end
|
|
329
317
|
binding.uses.filter_map do |use|
|
|
330
318
|
reference = use.reference
|
|
331
319
|
next unless reference.kind == :const
|
|
332
|
-
next unless privates.include?("#{
|
|
320
|
+
next unless privates.include?("#{constant}::#{reference.member}")
|
|
333
321
|
next if rescued?(reference, %w[NameError])
|
|
334
322
|
|
|
335
323
|
error(
|
|
336
324
|
use.file,
|
|
337
325
|
reference.line,
|
|
338
326
|
"#{reference.name}::#{reference.member} is a private constant " \
|
|
339
|
-
"of the #{binding.
|
|
327
|
+
"of the #{binding.carton.name} Carton",
|
|
340
328
|
)
|
|
341
329
|
end
|
|
342
330
|
end
|
|
343
331
|
end
|
|
344
332
|
|
|
345
333
|
# The constant a Carton exports, or nil.
|
|
346
|
-
def
|
|
347
|
-
exports = @project.files.fetch(
|
|
334
|
+
def exported_constant(carton)
|
|
335
|
+
exports = @project.files.fetch(carton.entrypoint).exports
|
|
348
336
|
exports.first.value if exports.size == 1
|
|
349
337
|
end
|
|
350
338
|
|
|
339
|
+
# A binding with no reads may be dead code. Constants on a Carton's
|
|
340
|
+
# Public Surface are also used implicitly by its Importers.
|
|
341
|
+
def unused_imports
|
|
342
|
+
@bindings.all.filter_map do |binding|
|
|
343
|
+
next unless binding.uses.empty?
|
|
344
|
+
next if public_binding?(binding)
|
|
345
|
+
next if @reopenings.any? { |_, _, used| used.equal?(binding) }
|
|
346
|
+
|
|
347
|
+
warning(
|
|
348
|
+
binding.file,
|
|
349
|
+
binding.line,
|
|
350
|
+
"#{binding.name} is bound to an import of the #{binding.carton.name} " \
|
|
351
|
+
'Carton but never used',
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def public_binding?(binding)
|
|
357
|
+
return false if binding.local
|
|
358
|
+
|
|
359
|
+
carton = @project.owner(binding.file) or return false
|
|
360
|
+
exports = @project.files.fetch(carton.entrypoint).exports
|
|
361
|
+
|
|
362
|
+
exports.any? do |export|
|
|
363
|
+
name = export.value&.delete_prefix('::')
|
|
364
|
+
name &&
|
|
365
|
+
(binding.name == name || binding.name.start_with?("#{name}::"))
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Classes and modules returned by Imports are shared objects. Opening
|
|
370
|
+
# one, or defining a constant beneath it, mutates that shared object.
|
|
371
|
+
def reopened_classes
|
|
372
|
+
@reopenings.filter_map do |path, site, binding|
|
|
373
|
+
next if binding.carton == @project.owner(path)
|
|
374
|
+
|
|
375
|
+
warning(
|
|
376
|
+
path,
|
|
377
|
+
site.line,
|
|
378
|
+
"#{site.kind} #{site.name} opens #{binding.name} from the " \
|
|
379
|
+
"#{binding.carton.name} Carton, changing a shared class or module: " \
|
|
380
|
+
'subclass it or use a refinement instead',
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
351
385
|
def display(path) = Project.relative(path)
|
|
352
386
|
|
|
353
387
|
def error(file, line, message) = Problem.new(:error, file, line, message)
|
data/lib/carton/tools/cli.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
4
5
|
require 'optparse'
|
|
5
6
|
require_relative 'checks'
|
|
6
7
|
require_relative 'signatures'
|
|
@@ -81,28 +82,64 @@ module Carton
|
|
|
81
82
|
|
|
82
83
|
def check(args)
|
|
83
84
|
strict = false
|
|
85
|
+
format = 'text'
|
|
84
86
|
project =
|
|
85
87
|
project(args, 'check') do |o|
|
|
86
88
|
o.on('--strict', 'Fail on warnings too, not only errors') do
|
|
87
89
|
strict = true
|
|
88
90
|
end
|
|
91
|
+
o.on(
|
|
92
|
+
'--format FORMAT',
|
|
93
|
+
%w[text json],
|
|
94
|
+
'text (default) or json',
|
|
95
|
+
) { |value| format = value }
|
|
89
96
|
end
|
|
90
97
|
return 2 unless project
|
|
91
98
|
|
|
92
99
|
problems = Checks.new(project).problems
|
|
93
|
-
pwd = File.realpath(Dir.pwd)
|
|
94
|
-
problems.each do |problem|
|
|
95
|
-
file = Project.relative(problem.file)
|
|
96
|
-
message = problem.message.gsub("#{pwd}/", '').gsub(pwd, '.')
|
|
97
|
-
@out.puts "#{file}:#{problem.line}: #{problem.level}: #{message}"
|
|
98
|
-
end
|
|
99
100
|
errors = problems.count(&:error?)
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
summary = {
|
|
102
|
+
files: project.files.size,
|
|
103
|
+
cartons: project.cartons.size,
|
|
104
|
+
errors:,
|
|
105
|
+
warnings: problems.size - errors,
|
|
106
|
+
}
|
|
107
|
+
if format == 'json'
|
|
108
|
+
print_json(problems, summary)
|
|
109
|
+
else
|
|
110
|
+
print_text(problems, summary)
|
|
111
|
+
end
|
|
102
112
|
failures = strict ? problems.size : errors
|
|
103
113
|
failures.zero? ? 0 : 1
|
|
104
114
|
end
|
|
105
115
|
|
|
116
|
+
def print_text(problems, summary)
|
|
117
|
+
problems.each do |problem|
|
|
118
|
+
@out.puts "#{Project.relative(problem.file)}:#{problem.line}: " \
|
|
119
|
+
"#{problem.level}: #{relative_message(problem.message)}"
|
|
120
|
+
end
|
|
121
|
+
@out.puts "#{summary[:files]} files, #{summary[:cartons]} Cartons, " \
|
|
122
|
+
"#{summary[:errors]} errors, #{summary[:warnings]} warnings"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def print_json(problems, summary)
|
|
126
|
+
list =
|
|
127
|
+
problems.map do |problem|
|
|
128
|
+
{
|
|
129
|
+
level: problem.level,
|
|
130
|
+
file: Project.relative(problem.file),
|
|
131
|
+
line: problem.line,
|
|
132
|
+
message: relative_message(problem.message),
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
@out.puts JSON.pretty_generate(problems: list, **summary)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def relative_message(message)
|
|
139
|
+
pwd = File.realpath(Dir.pwd)
|
|
140
|
+
message.gsub("#{pwd}/", '').gsub(pwd, '.')
|
|
141
|
+
end
|
|
142
|
+
|
|
106
143
|
# Parse options shared by every command, then scan PATHs. Named imports
|
|
107
144
|
# resolve against `-I` directories, then this process's `$LOAD_PATH`,
|
|
108
145
|
# which includes the bundle's gems under `bundle exec`, and files belong
|
data/lib/carton/tools/project.rb
CHANGED
|
@@ -9,23 +9,23 @@ module Carton
|
|
|
9
9
|
# The Cartons an app imports, found by following `import` and
|
|
10
10
|
# `import_relative` calls from the given files without running them.
|
|
11
11
|
# Name-based imports resolve against `load_path`, as they would against the
|
|
12
|
-
#
|
|
12
|
+
# Importer's `$LOAD_PATH`. `specs` are the gems that files may belong
|
|
13
13
|
# to: under Bundler, the bundle's.
|
|
14
14
|
class Project
|
|
15
|
-
# A Carton: its Entrypoint, its files (the Entrypoint plus its
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
15
|
+
# A Carton: its Entrypoint, its files (the Entrypoint plus its Interior),
|
|
16
|
+
# and the `lib/` an Import of a Gem-shaped Directory adds to its load
|
|
17
|
+
# path.
|
|
18
|
+
Carton =
|
|
19
19
|
Data.define(:entrypoint, :files, :load_path) do
|
|
20
20
|
def name = File.basename(entrypoint, '.rb')
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# One import call and what it resolved to: a
|
|
24
|
-
# a name not found on the load path, which a gem activated at runtime
|
|
23
|
+
# One import call and what it resolved to: a Carton, an error message,
|
|
24
|
+
# or a name not found on the load path, which a gem activated at runtime
|
|
25
25
|
# may still provide.
|
|
26
|
-
Edge = Data.define(:call, :
|
|
26
|
+
Edge = Data.define(:call, :carton, :error, :unfound)
|
|
27
27
|
|
|
28
|
-
attr_reader :files, :
|
|
28
|
+
attr_reader :files, :cartons, :edges
|
|
29
29
|
|
|
30
30
|
# `path` relative to the current directory, for messages and comments.
|
|
31
31
|
def self.relative(path)
|
|
@@ -37,8 +37,8 @@ module Carton
|
|
|
37
37
|
@load_path = load_path
|
|
38
38
|
@specs = specs
|
|
39
39
|
@files = {}
|
|
40
|
-
@
|
|
41
|
-
@
|
|
40
|
+
@cartons = {}
|
|
41
|
+
@carton_by_file = {}
|
|
42
42
|
ruby_files(paths).each { |path| parse(path) }
|
|
43
43
|
discover
|
|
44
44
|
@edges = imports.map { |call| resolve(call) }
|
|
@@ -51,7 +51,7 @@ module Carton
|
|
|
51
51
|
return locate(call)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
@edge_by_call[call]&.
|
|
54
|
+
@edge_by_call[call]&.carton&.entrypoint
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# The gem whose require paths hold `path`, or nil.
|
|
@@ -62,8 +62,8 @@ module Carton
|
|
|
62
62
|
&.last
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# The
|
|
66
|
-
def owner(file) = @
|
|
65
|
+
# The Carton `file` belongs to, or nil for the app, which runs in Main.
|
|
66
|
+
def owner(file) = @carton_by_file[file]
|
|
67
67
|
|
|
68
68
|
def imports = @files.values.flat_map(&:imports)
|
|
69
69
|
|
|
@@ -86,9 +86,9 @@ module Carton
|
|
|
86
86
|
nil
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
# The
|
|
90
|
-
def
|
|
91
|
-
@
|
|
89
|
+
# The Carton whose files include `path`, if that Carton is imported.
|
|
90
|
+
def carton_containing(path)
|
|
91
|
+
@carton_by_file[path] || @cartons[Resolver.entrypoint_for(path)]
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
private
|
|
@@ -122,26 +122,26 @@ module Carton
|
|
|
122
122
|
required =
|
|
123
123
|
requires.select(&:relative?).filter_map { |call| locate(call) }
|
|
124
124
|
required = required.grep(/\.rb\z/).reject { |path| @files.key?(path) }
|
|
125
|
-
found = imports.filter_map { |call| resolve(call).
|
|
126
|
-
fresh = found.reject { |
|
|
125
|
+
found = imports.filter_map { |call| resolve(call).carton }
|
|
126
|
+
fresh = found.reject { |carton| @cartons.key?(carton.entrypoint) }
|
|
127
127
|
break if required.empty? && fresh.empty?
|
|
128
128
|
|
|
129
129
|
required.each { |path| parse(path) }
|
|
130
|
-
fresh.each { |
|
|
130
|
+
fresh.each { |carton| add_carton(carton) }
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
def
|
|
135
|
-
@
|
|
136
|
-
|
|
137
|
-
@
|
|
134
|
+
def add_carton(carton)
|
|
135
|
+
@cartons[carton.entrypoint] = carton
|
|
136
|
+
carton.files.each do |file|
|
|
137
|
+
@carton_by_file[file] ||= carton
|
|
138
138
|
parse(file)
|
|
139
139
|
end
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
def resolve(call)
|
|
143
143
|
unless call.path
|
|
144
|
-
return Edge.new(call:,
|
|
144
|
+
return Edge.new(call:, carton: nil, error: nil, unfound: false)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
base_dir = File.dirname(call.file) if call.relative?
|
|
@@ -152,24 +152,24 @@ module Carton
|
|
|
152
152
|
base_dir:,
|
|
153
153
|
)
|
|
154
154
|
unless File.absolute_path?(target.feature)
|
|
155
|
-
return Edge.new(call:,
|
|
155
|
+
return Edge.new(call:, carton: nil, error: nil, unfound: true)
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
-
Edge.new(call:,
|
|
158
|
+
Edge.new(call:, carton: carton_for(target), error: nil, unfound: false)
|
|
159
159
|
rescue ImportError, LoadError => e
|
|
160
|
-
Edge.new(call:,
|
|
160
|
+
Edge.new(call:, carton: nil, error: e.message, unfound: false)
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
-
def
|
|
163
|
+
def carton_for(target)
|
|
164
164
|
entrypoint = File.realpath(target.feature)
|
|
165
|
-
@
|
|
165
|
+
@cartons.fetch(entrypoint) do
|
|
166
166
|
dir = entrypoint.delete_suffix('.rb')
|
|
167
167
|
files =
|
|
168
168
|
Dir
|
|
169
169
|
.glob('**/*.rb', base: dir)
|
|
170
170
|
.sort
|
|
171
171
|
.map { |file| File.realpath(File.join(dir, file)) }
|
|
172
|
-
|
|
172
|
+
Carton.new(
|
|
173
173
|
entrypoint:,
|
|
174
174
|
files: [entrypoint, *files],
|
|
175
175
|
load_path: target.load_path,
|
|
@@ -7,8 +7,6 @@ module Carton
|
|
|
7
7
|
# RBS for a Project's imports, so a type checker like Steep knows what each
|
|
8
8
|
# `import` returns. An exported constant names the Carton's own type,
|
|
9
9
|
# which the Carton's RBS declares, and a keyword export is a record type.
|
|
10
|
-
# Bare Cartons each get a module under `Carton::Namespace`, typed from
|
|
11
|
-
# rbs-inline `#:` annotations and literal values, or `untyped`.
|
|
12
10
|
class Signatures
|
|
13
11
|
HEADER = <<~RBS
|
|
14
12
|
# Generated by `carton rbs` from the app's imports. Do not edit.
|
|
@@ -16,11 +14,10 @@ module Carton
|
|
|
16
14
|
|
|
17
15
|
def initialize(project)
|
|
18
16
|
@project = project
|
|
19
|
-
@names = namespace_names
|
|
20
17
|
end
|
|
21
18
|
|
|
22
19
|
def to_s
|
|
23
|
-
[HEADER, kernel, *
|
|
20
|
+
[HEADER, kernel, *constants].compact.join("\n")
|
|
24
21
|
end
|
|
25
22
|
|
|
26
23
|
private
|
|
@@ -33,10 +30,10 @@ module Carton
|
|
|
33
30
|
overloads =
|
|
34
31
|
%w[import import_relative].filter_map do |kind|
|
|
35
32
|
types =
|
|
36
|
-
edges_by_path(kind).filter_map do |path,
|
|
37
|
-
next unless
|
|
33
|
+
edges_by_path(kind).filter_map do |path, cartons|
|
|
34
|
+
next unless cartons.uniq.size == 1
|
|
38
35
|
|
|
39
|
-
type = type_of(
|
|
36
|
+
type = type_of(cartons.first)
|
|
40
37
|
"(#{path.inspect} path) -> #{type}" unless type == 'untyped'
|
|
41
38
|
end
|
|
42
39
|
next if types.empty?
|
|
@@ -59,45 +56,12 @@ module Carton
|
|
|
59
56
|
def edges_by_path(kind)
|
|
60
57
|
@project
|
|
61
58
|
.edges
|
|
62
|
-
.select { |edge| edge.
|
|
59
|
+
.select { |edge| edge.carton && edge.call.kind == kind }
|
|
63
60
|
.group_by { |edge| edge.call.path }
|
|
64
|
-
.transform_values { |edges| edges.map(&:
|
|
61
|
+
.transform_values { |edges| edges.map(&:carton) }
|
|
65
62
|
.sort
|
|
66
63
|
end
|
|
67
64
|
|
|
68
|
-
# Each Bare Carton as a module whose singleton methods and constants
|
|
69
|
-
# mirror its `Carton::Namespace`.
|
|
70
|
-
def namespaces
|
|
71
|
-
@names
|
|
72
|
-
.sort_by { |_, name| name }
|
|
73
|
-
.map do |unit, name|
|
|
74
|
-
members = surface(unit).map { |member| " #{member}" }
|
|
75
|
-
[
|
|
76
|
-
"# #{Project.relative(unit.entrypoint)}",
|
|
77
|
-
"module Carton::Namespace::#{name}",
|
|
78
|
-
' extend ::Carton::NamespaceMethods',
|
|
79
|
-
*members,
|
|
80
|
-
'end',
|
|
81
|
-
'',
|
|
82
|
-
].join("\n")
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# A Bare Carton's surface is what its files define at top level.
|
|
87
|
-
def surface(unit)
|
|
88
|
-
files = unit.files.map { |path| file(path) }
|
|
89
|
-
methods = files.flat_map { |f| f.methods.keys }.uniq
|
|
90
|
-
constants = files.flat_map { |f| f.constants.keys + top_definitions(f) }
|
|
91
|
-
[
|
|
92
|
-
*methods.map do |name|
|
|
93
|
-
"def self.#{name}: #{method_type(unit, name)}"
|
|
94
|
-
end,
|
|
95
|
-
*constants.uniq.map do |name|
|
|
96
|
-
"#{name}: #{constant_type(unit, name)}"
|
|
97
|
-
end,
|
|
98
|
-
]
|
|
99
|
-
end
|
|
100
|
-
|
|
101
65
|
# `CONST = import ...` in any file, unless the Carton's own RBS already
|
|
102
66
|
# declares that name: `Billing = import 'billing'` for `export Billing`.
|
|
103
67
|
# A constant bound to two different Cartons is skipped.
|
|
@@ -105,36 +69,31 @@ module Carton
|
|
|
105
69
|
bindings =
|
|
106
70
|
@project
|
|
107
71
|
.edges
|
|
108
|
-
.select { |edge| edge.
|
|
72
|
+
.select { |edge| edge.carton && edge.call.constant }
|
|
109
73
|
.group_by { |edge| edge.call.constant }
|
|
110
|
-
.transform_values { |edges| edges.map(&:
|
|
111
|
-
.select { |_,
|
|
74
|
+
.transform_values { |edges| edges.map(&:carton).uniq }
|
|
75
|
+
.select { |_, cartons| cartons.size == 1 }
|
|
112
76
|
lines =
|
|
113
|
-
bindings.sort.filter_map do |constant, (
|
|
114
|
-
type = type_of(
|
|
77
|
+
bindings.sort.filter_map do |constant, (carton)|
|
|
78
|
+
type = type_of(carton)
|
|
115
79
|
next if type == "singleton(::#{constant})"
|
|
116
80
|
|
|
117
|
-
alias_of(constant,
|
|
81
|
+
alias_of(constant, carton, type) || "#{constant}: #{type}"
|
|
118
82
|
end
|
|
119
83
|
return if lines.empty?
|
|
120
84
|
|
|
121
85
|
['# Constants bound to imports', *lines, ''].join("\n")
|
|
122
86
|
end
|
|
123
87
|
|
|
124
|
-
def alias_of(constant,
|
|
88
|
+
def alias_of(constant, carton, type)
|
|
125
89
|
target = type[/\Asingleton\((.+)\)\z/, 1] or return
|
|
126
|
-
return "module #{constant} = #{target}" if @names.key?(unit)
|
|
127
90
|
|
|
128
|
-
kind = definition_kind(
|
|
91
|
+
kind = definition_kind(carton, target.delete_prefix('::'))
|
|
129
92
|
"#{kind} #{constant} = #{target}" if kind
|
|
130
93
|
end
|
|
131
94
|
|
|
132
|
-
def type_of(
|
|
133
|
-
|
|
134
|
-
return "singleton(::Carton::Namespace::#{@names[unit]})"
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
exports = file(unit.entrypoint).exports
|
|
95
|
+
def type_of(carton)
|
|
96
|
+
exports = file(carton.entrypoint).exports
|
|
138
97
|
return 'untyped' unless exports.size == 1
|
|
139
98
|
|
|
140
99
|
export = exports.first
|
|
@@ -145,76 +104,15 @@ module Carton
|
|
|
145
104
|
"{ #{fields.join(', ')} }"
|
|
146
105
|
end
|
|
147
106
|
|
|
148
|
-
def exported?(unit) = file(unit.entrypoint).exports.any?
|
|
149
|
-
|
|
150
107
|
def file(path) = @project.files.fetch(path)
|
|
151
108
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
definition = file(path).methods[name]
|
|
155
|
-
return definition.type if definition
|
|
156
|
-
end
|
|
157
|
-
'(?) -> untyped'
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def constant_type(unit, name)
|
|
161
|
-
unit.files.each do |path|
|
|
162
|
-
type = file(path).constants[name]
|
|
163
|
-
return type if type
|
|
164
|
-
end
|
|
165
|
-
kind = definition_kind(unit, name)
|
|
166
|
-
kind ? "singleton(::#{name})" : 'untyped'
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def definition_kind(unit, name)
|
|
170
|
-
unit.files.each do |path|
|
|
109
|
+
def definition_kind(carton, name)
|
|
110
|
+
carton.files.each do |path|
|
|
171
111
|
kind = file(path).definitions[name]
|
|
172
112
|
return kind if kind
|
|
173
113
|
end
|
|
174
114
|
nil
|
|
175
115
|
end
|
|
176
|
-
|
|
177
|
-
def top_definitions(entry)
|
|
178
|
-
entry.definitions.keys.reject { |name| name.include?('::') }
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# A module name under `Carton::Namespace` for each Bare Carton: its
|
|
182
|
-
# Entrypoint's name, camelized, with parent directories prepended while
|
|
183
|
-
# two Cartons share a name.
|
|
184
|
-
def namespace_names
|
|
185
|
-
units =
|
|
186
|
-
@project
|
|
187
|
-
.edges
|
|
188
|
-
.filter_map(&:unit)
|
|
189
|
-
.uniq
|
|
190
|
-
.reject { |unit| exported?(unit) }
|
|
191
|
-
depth = units.to_h { |unit| [unit, 1] }
|
|
192
|
-
loop do
|
|
193
|
-
names = units.to_h { |unit| [unit, camelize(unit, depth[unit])] }
|
|
194
|
-
clashes = names.keys.group_by { |unit| names[unit] }.values
|
|
195
|
-
clashes = clashes.select { |group| group.size > 1 }.flatten
|
|
196
|
-
if clashes.empty? || clashes.none? { |unit| deeper?(unit, depth) }
|
|
197
|
-
return names
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
clashes.each { |unit| depth[unit] += 1 }
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def deeper?(unit, depth) = depth[unit] < path_parts(unit).size
|
|
205
|
-
|
|
206
|
-
def camelize(unit, depth)
|
|
207
|
-
words = path_parts(unit).last(depth).join('_').split(/[^A-Za-z0-9]+/)
|
|
208
|
-
words.map { |word| word[0].upcase + word[1..] }.join
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def path_parts(unit)
|
|
212
|
-
unit
|
|
213
|
-
.entrypoint
|
|
214
|
-
.delete_suffix('.rb')
|
|
215
|
-
.split('/')
|
|
216
|
-
.reject { |part| part.empty? || part == 'lib' }
|
|
217
|
-
end
|
|
218
116
|
end
|
|
219
117
|
end
|
|
220
118
|
end
|
|
@@ -38,6 +38,10 @@ module Carton
|
|
|
38
38
|
Reference =
|
|
39
39
|
Data.define(:name, :local, :scope, :kind, :member, :line, :rescued)
|
|
40
40
|
|
|
41
|
+
# A class or module definition, with its name as written and the
|
|
42
|
+
# enclosing namespace, before entering its body.
|
|
43
|
+
Definition = Data.define(:name, :scope, :kind, :line)
|
|
44
|
+
|
|
41
45
|
# A hash pattern matched directly against an import: its Reference
|
|
42
46
|
# `kind` (:fetch for `=>`, :lookup for `in`) and the keys it reads, with
|
|
43
47
|
# `'**'` for a rest pattern.
|
|
@@ -61,9 +65,6 @@ module Carton
|
|
|
61
65
|
end
|
|
62
66
|
end
|
|
63
67
|
|
|
64
|
-
# A top-level method and its RBS method type.
|
|
65
|
-
Def = Data.define(:name, :type, :line)
|
|
66
|
-
|
|
67
68
|
IMPORTS = %i[import import_relative].freeze
|
|
68
69
|
REQUIRES = %i[require require_relative].freeze
|
|
69
70
|
|
|
@@ -71,10 +72,11 @@ module Carton
|
|
|
71
72
|
:imports,
|
|
72
73
|
:requires,
|
|
73
74
|
:exports,
|
|
74
|
-
:
|
|
75
|
+
:export_lines,
|
|
75
76
|
:constants,
|
|
76
77
|
:definitions,
|
|
77
78
|
:references,
|
|
79
|
+
:definition_sites,
|
|
78
80
|
:private_constants,
|
|
79
81
|
:local_writes,
|
|
80
82
|
:errors
|
|
@@ -86,12 +88,15 @@ module Carton
|
|
|
86
88
|
@imports = []
|
|
87
89
|
@requires = []
|
|
88
90
|
@exports = []
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
+
# Every possible export call, including ones whose execution or type
|
|
92
|
+
# cannot be inferred, so missing-export checks stay conservative.
|
|
93
|
+
@export_lines = []
|
|
94
|
+
# RBS types of top-level constants.
|
|
91
95
|
@constants = {}
|
|
92
96
|
# Every class and module defined, by full name: :class or :module.
|
|
93
97
|
@definitions = {}
|
|
94
98
|
@references = []
|
|
99
|
+
@definition_sites = []
|
|
95
100
|
# Full names made private with `private_constant`.
|
|
96
101
|
@private_constants = []
|
|
97
102
|
# How many times each local variable is assigned, so a local bound to
|
|
@@ -116,12 +121,6 @@ module Carton
|
|
|
116
121
|
end
|
|
117
122
|
end
|
|
118
123
|
|
|
119
|
-
# The rbs-inline `#: TYPE` annotation on `line`, if any.
|
|
120
|
-
def annotation(line)
|
|
121
|
-
text = @annotations[line]
|
|
122
|
-
text[2..].strip if text&.start_with?('#:')
|
|
123
|
-
end
|
|
124
|
-
|
|
125
124
|
# The RBS type of a literal value node, or `untyped`.
|
|
126
125
|
def self.literal_type(node)
|
|
127
126
|
case node
|
|
@@ -150,36 +149,6 @@ module Carton
|
|
|
150
149
|
end
|
|
151
150
|
end
|
|
152
151
|
|
|
153
|
-
# An RBS method type with untyped parameters matching a `def`.
|
|
154
|
-
def self.method_type(parameters)
|
|
155
|
-
return '() -> untyped' unless parameters
|
|
156
|
-
|
|
157
|
-
params = parameters.requireds.map { |node| positional(node) }
|
|
158
|
-
parameters.optionals.each { |node| params << "?untyped #{node.name}" }
|
|
159
|
-
if parameters.rest.is_a?(Prism::RestParameterNode)
|
|
160
|
-
params << "*untyped #{parameters.rest.name}".strip
|
|
161
|
-
end
|
|
162
|
-
parameters.posts.each { |node| params << positional(node) }
|
|
163
|
-
parameters.keywords.each do |node|
|
|
164
|
-
optional = node.is_a?(Prism::OptionalKeywordParameterNode)
|
|
165
|
-
params << "#{'?' if optional}#{node.name}: untyped"
|
|
166
|
-
end
|
|
167
|
-
block = parameters.block ? ' ?{ (?) -> untyped }' : ''
|
|
168
|
-
case parameters.keyword_rest
|
|
169
|
-
when Prism::KeywordRestParameterNode
|
|
170
|
-
params << "**untyped #{parameters.keyword_rest.name}".strip
|
|
171
|
-
when Prism::ForwardingParameterNode
|
|
172
|
-
params.push('*untyped', '**untyped')
|
|
173
|
-
block = ' ?{ (?) -> untyped }'
|
|
174
|
-
end
|
|
175
|
-
"(#{params.join(', ')})#{block} -> untyped"
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def self.positional(node)
|
|
179
|
-
node.respond_to?(:name) ? "untyped #{node.name}" : 'untyped'
|
|
180
|
-
end
|
|
181
|
-
private_class_method :positional
|
|
182
|
-
|
|
183
152
|
# Walks the tree once, tracking the class and module nesting and whether
|
|
184
153
|
# code runs at load time.
|
|
185
154
|
class Visitor < Prism::Visitor
|
|
@@ -243,6 +212,10 @@ module Carton
|
|
|
243
212
|
end
|
|
244
213
|
|
|
245
214
|
def visit_call_node(node)
|
|
215
|
+
if node.name == :export &&
|
|
216
|
+
(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
217
|
+
@file.export_lines << node.location.start_line
|
|
218
|
+
end
|
|
246
219
|
record_call(node) if node.receiver.nil?
|
|
247
220
|
record_use(node) if bindable?(node.receiver)
|
|
248
221
|
super
|
|
@@ -295,16 +268,7 @@ module Carton
|
|
|
295
268
|
match(node.value, node.pattern, :lookup) { super }
|
|
296
269
|
end
|
|
297
270
|
|
|
298
|
-
def visit_def_node(node)
|
|
299
|
-
if top_level? && node.receiver.nil?
|
|
300
|
-
line = node.location.start_line
|
|
301
|
-
type =
|
|
302
|
-
@file.annotation(line - 1) ||
|
|
303
|
-
SourceFile.method_type(node.parameters)
|
|
304
|
-
@file.methods[node.name.to_s] = Def.new(node.name.to_s, type, line)
|
|
305
|
-
end
|
|
306
|
-
nest(:@defs) { super }
|
|
307
|
-
end
|
|
271
|
+
def visit_def_node(node) = nest(:@defs) { super }
|
|
308
272
|
|
|
309
273
|
def visit_block_node(node) = nest(:@blocks) { super }
|
|
310
274
|
|
|
@@ -360,9 +324,7 @@ module Carton
|
|
|
360
324
|
def visit_constant_write_node(node)
|
|
361
325
|
name = qualify(node.name.to_s)
|
|
362
326
|
if top_level?
|
|
363
|
-
@file.constants[name] =
|
|
364
|
-
node.location.start_line,
|
|
365
|
-
) || SourceFile.literal_type(node.value)
|
|
327
|
+
@file.constants[name] = SourceFile.literal_type(node.value)
|
|
366
328
|
end
|
|
367
329
|
binding(:@constant, name, node.value) { super }
|
|
368
330
|
end
|
|
@@ -584,6 +546,13 @@ module Carton
|
|
|
584
546
|
name = constant_name(node.constant_path)
|
|
585
547
|
return nest(:@defs, 0, &) unless name
|
|
586
548
|
|
|
549
|
+
@file.definition_sites << Definition.new(
|
|
550
|
+
name:,
|
|
551
|
+
scope: @namespace.last,
|
|
552
|
+
kind:,
|
|
553
|
+
line: node.location.start_line,
|
|
554
|
+
)
|
|
555
|
+
|
|
587
556
|
name = qualify(name)
|
|
588
557
|
@file.definitions[name] ||= kind
|
|
589
558
|
@namespace.push(name)
|
data/lib/carton/tools/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: carton-tools
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cristofaro
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.4.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.4.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: prism
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,7 +37,7 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.8'
|
|
40
|
-
description: The carton command for Carton
|
|
40
|
+
description: The carton command for Carton apps with explicit imports and exports.
|
|
41
41
|
carton check reports import and export problems and carton rbs writes RBS for what
|
|
42
42
|
each import returns, reading code with Prism without running it. Requires Ruby 4.0.7+.
|
|
43
43
|
email:
|