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