Class: Manager

Inherits:
Object
  • Object
show all
Defined in:
lib/manager.rb

Defined Under Namespace

Modules: Coda Classes: Spec

Constant Summary collapse

DebugDirs =
[
	$0,
	File.expand_path("#{__dir__}/../bin"),
	__dir__,
	Gem.loaded_specs["benchmark-ips"].gem_dir,
]
Main =
TOPLEVEL_BINDING.receiver
AlternativeMethod =

! This format must be applicable to methods whose normal form ends with ‘?`, `!`, or `=`. Also, it must save the alternative part like `2` from being in the first part of the method, to

avoid syntax error.
/\A(?!.*__.+__)(.+)__[^?!=]+([?!=]?)\z/
InterruptionInactive =
Proc.new{print "\b\b\b\b\b"}
CodeRayOption =
{tab_width: 2, css: :class}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(spec, **command_options) ⇒ Manager

Returns a new instance of Manager.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/manager.rb', line 118

def initialize spec, **command_options
	Signal.trap("INT", &InterruptionInactive)
	@files, @specs, @described_headers, @implementations, @annotations, @context, @counts =
	[], {}, {}, {}, {}, Context.new, Hash.new(0)
	self.class.current = self
	abort "Spec file not found" unless spec.kind_of?(String)
	abort "Spec file not found: #{spec}" unless File.exist?(spec)
	spec_s, _, data = File.read(spec).partition(/^__END__[ \t]*\n/)
	l = spec_s.count($/)
	@files.push([spec, {mtime: File.new(spec).mtime, lines: l}])
	manage(spec, data, l) unless data.empty?
	Coverage.start
	ObjectSpace.trace_object_allocations_start
	begin
		load(spec)
	rescue Exception => e
		Console.abort(e)
	ensure
		ObjectSpace.trace_object_allocations_stop
	end
	#! command line options override options given in spec files
	Manager.config(command_options)
	bdir = File.expand_path(Manager.config(:bdir), File.dirname(spec))
	Console.abort("Configured \"#{bdir}\" is not a directory") unless File.directory?(bdir)
	Manager.config(bdir_expanded: bdir)
	odir = File.expand_path(Manager.config(:odir), File.dirname(spec))
	Console.abort("Configured \"#{odir}\" is not a directory") unless File.directory?(odir)
	title = Manager.config(:title) || (@files.dig(1, 0) && File.basename(@files[1][0], ".*"))
	print "Combining spec with information gathered from source..."; $stdout.flush
	@implementations.each do
		|(modul, type, alt), location|
		@specs[modul] ||= {[:module, nil] => Spec.new.undocumented_mark}
		next if type == :module
		feature = Manager.main_method(alt)
		begin @specs[modul][[type, feature]] ||=
		case type
		when :module_as_constant then Spec.new
		when :instance, :singleton, :constant then Spec.new.undocumented_mark
		end end
		.alts[alt] = nil
	end
	@annotations.each do
		|(modul, type, feature), h|
		global = modul == Main
		if feature == nil
			@specs[modul] ||= {[:module, nil] => Spec.new}
			search_items = @specs[modul]
			.each_with_object([]){|((type, _), spec), a| a.concat(spec.items) if type == :module}
			#! Global annotations have the file name for `type`.
			#   Local `main` annotations have `nil` for `type`.
			target_items = @specs[modul][[:module, feature]].items
		else
			search_items =
			target_items = @specs[modul][[type, feature]].items
		end
		h.each do
			|tag, (locations, text)|
			search_items.any?{|e| Render::Annotation.concat?(e, global, tag, locations, text)} or
			target_items.push(Render::Annotation.new(global, tag, locations, text))
		end
	end
	print "done\r"; $stdout.flush
	print "Organizing items..."; $stdout.flush
	@specs[Object] &.reject! do
		|(type, _), spec| type == :module_as_constant and spec.items.empty?
	end
	@specs.delete(Object) if begin
		@specs[Object] &.length == 1 and @specs[Object][[:module, nil]].items.empty?
	end
	@specs.each do
		|modul, h|
		unless modul == Main
			#! Main only has `:module` item. `aliases` is used only with
			#    `:instance` and `:singleton` items.
			aliases = {instance: modul.aliases, singleton: modul.singleton_class.aliases}
			#! Must refer from child to ancestors, not the other way around because it is not 
			#   guaranteed that the child is ordered later than the ancestors within `@specs`.
			namespaces = modul.namespace
			module_documentation = namespaces
			.find{|m| break e if e = @specs.dig(m, [:module, nil]) &.documentation}
			module_hidden = namespaces
			.find{|m| break e if e = @specs.dig(m, [:module, nil]) &.hidden}
			h[[:module, nil]].hidden = true if module_hidden
		end
		h.each do
			|(type, feature), spec|
			if spec.hidden
				spec.items.each do
					|e| case e; when Render::UserItem
						Console.abort wrong_item(
						e.source_item || e, "A hidden feature has a user document item")
					end
				end
			end
			spec.order_fix
			case type
			when :module
				case feature
				when Render::DescribedHeader
					spec.header = feature
				when nil
					if spec.hidden
						h.each do |_, spec| spec.items.each do
							|e| case e; when Render::UserItem
								Console.abort wrong_item(
								e.source_item || e, "A hidden feature has a user document item")
							end
						end end
					end
					case modul
					when Main
						spec.header = Render::NilHeader
					else
						visibility = modul.constant_visibility(nil)
						spec.header =
						(spec.hidden ?
						Render::DevModuleHeader : Render::UserModuleHeader)
						.new(modul, spec, visibility)
						spec.items.unshift(Render::AncestorDiagrams.new(modul))
					end
				end
			when :module_as_constant
				spec.type = nil
				spec.alts[feature] = nil
				spec.alts.each_key{|alt| spec.alts[alt] = modul.const_get(alt.to_s)}
				spec.header =
				(module_hidden || spec.hidden ?
				Render::DevFeatureHeader : Render::UserFeatureHeader)
				.new(modul, type, feature, spec)
			when :constant
				spec.missing?(type, module_documentation, module_hidden)
				spec.alts[feature] = nil
				spec.alts.each_key{|alt| spec.alts[alt] =
				  modul.constant_value(alt, module_hidden || spec.hidden)}
				visibility = spec.alts[feature] &.visibility
				location = spec.alts[feature] &.location
				inherited = visibility && modul.inherited?(type, feature)
				#! Unlike methods, unknown (i.e. `location == nil`) does not count as `different_file`.
				#   It may or may not belong to an unlisted source location, but as of now,
				#   there is no way to tell.
				different_file = visibility && location && (@files.assoc(location &.[](0)).! || nil)
				spec.documentation ||= (inherited || different_file) && module_documentation.! &&
				Render::Tag.new(true, "Misplaced", message: [
					(inherited && "Defined on `#{inherited.inspect}`."),
					(different_file && "Not defined in a listed file."),
				].join(" "))
				spec.header =
				(module_hidden || spec.hidden ?
				Render::DevFeatureHeader : Render::UserFeatureHeader)
				.new(modul, type, feature, spec)
				spec.items.unshift(
					spec.alts.values.first,
					Render::Assignment.new(visibility, location)
				)
			when :instance, :singleton
				spec.missing?(type, module_documentation, module_hidden)
				spec.aliases = aliases.dig(type, feature)
				if spec.alts.key?(feature).!
					spec.alts[feature] = nil
					spec.alts = spec.alts.sort.to_h
				end
				spec.alts.each_key{|alt| spec.alts[alt] = modul.implementation(type, alt)}
				visibility = spec.alts[feature] &.visibility
				location = spec.alts[feature] &.location
				inherited = visibility && modul.inherited?(type, feature)
				different_file = visibility && (@files.assoc(location &.[](0)).! || nil)
				original_name = visibility && modul.original_name(type, feature)
				aliasing =  (original_name unless original_name == feature) || nil
				spec.documentation ||= (inherited || different_file || aliasing) &&
				module_documentation.! &&
				Render::Tag.new(true, "Misplaced", message: [
					(inherited && "Defined on `#{inherited.inspect}`."),
					(different_file && "Not defined in a listed file."),
					(aliasing && "Defined as alias of `#{aliasing}`.")
				].join(" "))
				spec.header =
				(module_hidden || spec.hidden ?
				Render::DevFeatureHeader : Render::UserFeatureHeader)
				.new(modul, type, feature, spec)
				spec.items.unshift(Render::Implementations.new(spec.alts.values))
			end
			#! The first key (the main key) can potentially have a `nil` value.
			test_alts = spec.alts.any?{|_, v| v} ? spec.alts.select{|_, v| v} : spec.alts
			@context.new_feature(modul, type, test_alts.keys)
			#! Symbol to proc cannot be used here because `evaluate` is refinement
			spec.items.map!{|e| e.evaluate}
		end
	end
	@top = Render::Top.new
	ObjectSpace.trace_object_allocations_clear
	print "Processing coverage..."; $stdout.flush
	@coverage = Coverage.result
	if Manager.config(:coverage)
		#! Drop the spec file
		@files.drop(1).each do
			|f, _|
			puts f
			l_i = @coverage[f].length.to_s.length
			l_s = @coverage[f].compact.max.to_s.length
			puts(@coverage[f].zip(File.readlines(f)).map.with_index(1) do
				|(n, l), i|
				s = (n || "-").to_s.rjust(l_s)
				s =
				case n
				when 0 then "\e[31m#{s}\e[m"
				else "\e[32m#{s}\e[m"
				end
				"#{i.to_s.rjust(l_i)} #{s} #{l}"
			end)
		end
	end
	print "done\r"; $stdout.flush
	print "Rendering user's manual...\r"; $stdout.flush
	size = File.write(user = "#{odir}/#{Manager.config(:user)}", render(mode: :user, title: title))
	puts "Wrote #{user} (#{size.to_s.gsub(/(?<=\d)(?=(?:\d{3})+\z)/, ",")} bytes)."
	print "Rendering developer's chart...\r"; $stdout.flush
	size = File.write(dev = "#{odir}/#{Manager.config(:dev)}", render(mode: :dev, title: title))
	puts "Wrote #{dev} (#{size.to_s.gsub(/(?<=\d)(?=(?:\d{3})+\z)/, ",")} bytes)."
end

Instance Attribute Details

#annotation_extractorObject

Returns the value of attribute annotation_extractor.



116
117
118
# File 'lib/manager.rb', line 116

def annotation_extractor
  @annotation_extractor
end

#annotationsObject

Returns the value of attribute annotations.



116
117
118
# File 'lib/manager.rb', line 116

def annotations
  @annotations
end

#contextObject

Returns the value of attribute context.



116
117
118
# File 'lib/manager.rb', line 116

def context
  @context
end

#countsObject

Returns the value of attribute counts.



116
117
118
# File 'lib/manager.rb', line 116

def counts
  @counts
end

#featureObject (readonly)

Returns the value of attribute feature.



117
118
119
# File 'lib/manager.rb', line 117

def feature
  @feature
end

#filesObject

Returns the value of attribute files.



116
117
118
# File 'lib/manager.rb', line 116

def files
  @files
end

#implementationsObject

Returns the value of attribute implementations.



116
117
118
# File 'lib/manager.rb', line 116

def implementations
  @implementations
end

#modulObject (readonly)

Returns the value of attribute modul.



117
118
119
# File 'lib/manager.rb', line 117

def modul
  @modul
end

#sampleObject (readonly)

Returns the value of attribute sample.



117
118
119
# File 'lib/manager.rb', line 117

def sample
  @sample
end

#slfObject (readonly)

Returns the value of attribute slf.



117
118
119
# File 'lib/manager.rb', line 117

def slf
  @slf
end

#typeObject (readonly)

Returns the value of attribute type.



117
118
119
# File 'lib/manager.rb', line 117

def type
  @type
end

Class Method Details

.config(k = nil, bdir: nil, bdir_expanded: nil, odir: nil, user: nil, dev: nil, theme: nil, highlight: nil, debug: nil, spell_check: nil, case_sensitive: nil, case_insensitive: nil, spell_checker: nil, spell_check_regex: nil, timeout: nil, title: nil, coverage: nil) ⇒ Object

! ‘coverage` is a temporal workaround



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/manager.rb', line 81

def self.config k = nil, bdir: nil, bdir_expanded: nil, odir: nil, user: nil, dev: nil, theme: nil, highlight: nil, debug: nil, spell_check: nil, case_sensitive: nil, case_insensitive: nil, spell_checker: nil, spell_check_regex: nil, timeout: nil, title: nil, coverage: nil #! `coverage` is a temporal workaround
	return @config[k] if k
	@config[:bdir] = bdir if bdir
	@config[:bdir_expanded] = Pathname.new(bdir_expanded) if bdir_expanded
	@config[:odir] = odir if odir
	@config[:user] = user if user
	@config[:dev] = dev if dev
	@config[:theme] = File.expand_path("#{__dir__}/../theme/#{theme}") if theme
	@config[:highlight] = File.expand_path("#{__dir__}/../theme/#{highlight}") if highlight
	@config[:debug] = debug if debug == true or debug == false
	if spell_check
		spell_check = spell_check.to_s
		if Spellcheck.language?(spell_check)
			@config[:spell_check] = spell_check
		else
			raise "The configured spell checking language `#{spell_check}` is not available. "\
			"Either install the corresponding aspell language component, or change "\
			"the `spell_check` option."
		end
	end
	if case_sensitive
		@config[:case_sensitive] =
		case_sensitive.each_with_object({}){|w, h| h[w] = true}
	end
	if case_insensitive
		@config[:case_insensitive] =
		case_insensitive.each_with_object({}){|w, h| h[w.downcase] = true}
	end
	@config[:spell_checker] = spell_checker if spell_checker
	@config[:spell_check_regex] = spell_check_regex if spell_check_regex
	@config[:timeout] = timeout if timeout
	@config[:title] = title if title
	@config[:coverage] = coverage
	nil
end

.contextObject



78
# File 'lib/manager.rb', line 78

def self.context; current.context end

.countsObject



79
# File 'lib/manager.rb', line 79

def self.counts; current.counts end

.main_method(sym) ⇒ Object



470
471
472
# File 'lib/manager.rb', line 470

def self.main_method sym
	AlternativeMethod.match(sym) &.captures &.join &.to_sym || sym
end

.validate_feature_call(modul, feature, coda) ⇒ Object



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/manager.rb', line 447

def self.validate_feature_call modul, feature, coda
	#! When `coda` is an instance of `Manager::UnitTest`, which is a child of `BasicObject`, 
	# `coda ==` would not be defined. Hence this order. The `==` is not symmetric.
	current.bad_spec "Missing `coda` as the last argument" unless Coda == coda
	case feature
	when AlternativeMethod
		raise "The feature name crashes with alternative"\
		" method name format reserved by manager gem, and cannot be used: #{feature}"
	when /\A::(.+)/
		[:constant, $1.to_sym]
	when /\A\.(.+)/
		[:singleton, $1.to_sym]
	when /\A#(.+)/
		[:instance, $1.to_sym]
	when /\A=/
		feature.gsub!(/\A(=+) ?/, "")
		[:module, current.add_described_header(modul, $1.length, feature)]
	when nil
		[:module, nil]
	else
		raise current.wrong_item(feature, "Invalid feature name")
	end
end

Instance Method Details

#_spec(slf, type, feature, items) ⇒ Object



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/manager.rb', line 422

def _spec slf, type, feature, items
	modul = slf == Main ? Object : slf
	case type
	when :module
		@modul = slf
	when :instance, :singleton
		@modul = modul
		@sample = slf
	when :constant
		@modul = modul
		@sample = modul
		if Module === (m = (modul.const_get(feature.to_s) rescue nil))
			@specs[m] ||= {[:module, nil] => Spec.new}
			type = :module_as_constant
		end
	end
	h = @specs[@modul] ||= {[:module, nil] => Spec.new}
	@type, @feature = type, feature
	spec = h[[@type, @feature]] ||= Spec.new
	#! Cannot use symbol to proc because `to_manager_object` is refined.
	spec.items.concat(items.map{|item| item.to_manager_object})
	#! Return value to pass to `hide` or `move`.
	spec
end

#add_described_header(modul, depth, feature) ⇒ Object



476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/manager.rb', line 476

def add_described_header modul, depth, feature
	h = @described_headers[modul] ||= {}
	k = depth == 1 ? [] : h.reverse_each.find do
		|k, _|
		if k.length == depth and k.last == feature
			raise wrong_item(feature, "Described feature name crash")
		end
		k.length == depth - 1
	end &.first
	raise wrong_item(feature, "Invalid depth for described feature") unless k
	h[[*k, feature]] = Render::DescribedHeader.new(modul, depth, feature)
end

#bad_spec(message) ⇒ Object



493
494
495
496
497
498
499
500
# File 'lib/manager.rb', line 493

def bad_spec message
	raise +"In" <<
	[
		@files.first.first,
		"#{caller.find{|l| l.start_with?(@files.first.first)} &.split(":") &.[](1)}",
		message,
	].join(":")
end

#described_headers(modul, names_path) ⇒ Object



488
489
490
491
492
# File 'lib/manager.rb', line 488

def described_headers modul, names_path
	@described_headers[modul]
	&.select{|k, _| k.last(names_path.length) == names_path}
	&.group_by{|k, _| k.length} &.min_by(&:first) &.last &.map(&:last)
end

#gemspec(f) ⇒ Object



337
# File 'lib/manager.rb', line 337

def gemspec f; @gemspec = Gem::Specification.load(f) end

#hide(spec) ⇒ Object



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/manager.rb', line 389

def hide spec
	bad_spec " `hide` must be followed by `spec`." unless spec.instance_of?(Spec)
	case @type
	when :module
		bad_spec " `hide` can only apply to a method or a constant (including module)."
	when :module_as_constant
		modul = @specs[@modul.const_get(@feature.to_s)][[:module, nil]]
		bad_spec " Conflicting `hide` and `move`." if modul.documentation and modul.hidden.!
		modul.documentation = Render::Tag.new(true, "Hidden")
		modul.hidden = true
		spec.hidden = true
	when :instance, :singleton, :constant
		bad_spec " Conflicting `hide` and `move`." if spec.documentation and spec.hidden.!
		spec.documentation = Render::Tag.new(true, "Hidden")
		spec.hidden = true
	end
	nil
end

#i(modul, type, feature) ⇒ Object



473
474
475
# File 'lib/manager.rb', line 473

def i modul, type, feature
	@specs.dig(modul, [type, feature]) &.i
end

#manage(f, data = nil, l = nil) ⇒ Object



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/manager.rb', line 338

def manage f, data = nil, l = nil
	#! The ordering between the following two lines is important. If done otherwise,
	#   the same file called through different paths would be loaded multiple times.
	return Dir.entries(f).each{|f| manage(f)} if File.directory?(f) unless f.nil?
	return if data.nil? and @files.any?{|k, _| k == f}
	begin
		l ||= File.read(f) &.count($/) unless f.nil?
	rescue Errno::ENOENT => e
		raise "#{e.message}\n"\
		"Called from #{caller_locations[2].absolute_path}:#{caller_locations[2].lineno}"
	end
	@files.push([f, {mtime: f && File.new(f) &.mtime, lines: (data ? data.count($/) + 1 : l)}])
	#!`nil` denotes that "source unknown" methods will be immune from "misplaced" error.
	return if f.nil?
	@annotation_extractor = AnnotationExtractor.new(f)
	tracing_classes = {}
	constants_stack = []
	Object.module_start(constants_stack, tracing_classes)
	tp_module_start = TracePoint.new(:class) do
		|tp|
		modul, _f, l = tp.binding.receiver, tp.path, tp.lineno
		next unless _f == f
		@annotation_extractor.read_upto(modul, :module, nil, [_f, l])
		modul.module_start(constants_stack, tracing_classes)
	end
	tp_module_end = TracePoint.new(:end) do
		|tp|
		modul, _f, l = tp.binding.receiver, tp.path, tp.lineno
		next unless _f == f
		@annotation_extractor.read_upto(modul, :module_end, nil, [_f, l])
		modul.module_end(constants_stack)
	end
	tp_module_start.enable
	tp_module_end.enable
	begin
		#! `l` for the spec code lines, `1` for the `__END__` line, and `1` for starting at 1.
		data ? TOPLEVEL_BINDING.eval(data, f, l + 1 + 1) : load(f)
	rescue Exception => e
		Console.abort(e)
	end
	tp_module_start.disable
	tp_module_end.disable
	Object.module_end(constants_stack)
	tracing_classes.each_key do
		|modul|
		modul.singleton_class.__send__(:remove_method, :singleton_method_added)
		modul.singleton_class.__send__(:remove_method, :method_added)
	end
	@annotation_extractor.close
	nil
end

#move(spec) ⇒ Object



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/manager.rb', line 407

def move spec
	bad_spec " `move` must be followed by `spec`." unless spec.instance_of?(Spec)
	case @type
	when :module
		bad_spec " `move` can only apply to a method or a constant (including module)."
	when :module_as_constant
		modul = @specs[@modul.const_get(@feature.to_s)][[:module, nil]]
		bad_spec " Conflicting `hide` and `move`." if modul.documentation and modul.hidden
		modul.documentation = Render::Tag.new(true, "Moved")
	when :instance, :singleton, :constant	
		bad_spec " Conflicting `hide` and `move`." if spec.documentation and spec.hidden
		spec.documentation = Render::Tag.new(true, "Moved")
	end
	nil
end

#wrong_item(item, message) ⇒ Object



501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/manager.rb', line 501

def wrong_item item, message
	RuntimeError.new((+"In ") <<
	[
		*if Manager.config(:debug) or Manager::DebugDirs
		.none?{|dir| ObjectSpace.allocation_sourcefile(item) &.start_with?(dir)}
			[ObjectSpace.allocation_sourcefile(item), ObjectSpace.allocation_sourceline(item)]
		else "?"
		end,
		message,
		item.inspect,
	].join(":"))
end