docrb-parser 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +75 -0
- data/Rakefile +12 -0
- data/docrb-parser.gemspec +38 -0
- data/lib/docrb/core_extensions.rb +60 -0
- data/lib/docrb/parser/attribute.rb +25 -0
- data/lib/docrb/parser/call.rb +27 -0
- data/lib/docrb/parser/class.rb +94 -0
- data/lib/docrb/parser/comment.rb +40 -0
- data/lib/docrb/parser/comment_parser.rb +290 -0
- data/lib/docrb/parser/computations.rb +471 -0
- data/lib/docrb/parser/constant.rb +19 -0
- data/lib/docrb/parser/container.rb +305 -0
- data/lib/docrb/parser/deferred_singleton_class.rb +17 -0
- data/lib/docrb/parser/location.rb +43 -0
- data/lib/docrb/parser/method.rb +62 -0
- data/lib/docrb/parser/method_parameters.rb +85 -0
- data/lib/docrb/parser/module.rb +50 -0
- data/lib/docrb/parser/node_array.rb +24 -0
- data/lib/docrb/parser/reference.rb +25 -0
- data/lib/docrb/parser/reloader.rb +19 -0
- data/lib/docrb/parser/resolved_reference.rb +26 -0
- data/lib/docrb/parser/version.rb +7 -0
- data/lib/docrb/parser/virtual_container.rb +21 -0
- data/lib/docrb/parser/virtual_location.rb +9 -0
- data/lib/docrb/parser/virtual_method.rb +19 -0
- data/lib/docrb/parser.rb +139 -0
- data/lib/docrb-parser.rb +3 -0
- data/sig/docrb/core_extensions.rbs +24 -0
- data/sig/docrb/parser/attribute.rbs +18 -0
- data/sig/docrb/parser/call.rbs +17 -0
- data/sig/docrb/parser/class.rbs +34 -0
- data/sig/docrb/parser/comment.rbs +14 -0
- data/sig/docrb/parser/comment_parser.rbs +79 -0
- data/sig/docrb/parser/constant.rbs +15 -0
- data/sig/docrb/parser/container.rbs +91 -0
- data/sig/docrb/parser/deferred_singleton_class.rbs +12 -0
- data/sig/docrb/parser/location.rbs +24 -0
- data/sig/docrb/parser/method.rbs +34 -0
- data/sig/docrb/parser/method_parameters.rbs +34 -0
- data/sig/docrb/parser/module.rbs +14 -0
- data/sig/docrb/parser/node_array.rbs +12 -0
- data/sig/docrb/parser/reference.rbs +19 -0
- data/sig/docrb/parser/reloader.rbs +7 -0
- data/sig/docrb/parser/resolved_reference.rbs +22 -0
- data/sig/docrb/parser/virtual_method.rbs +17 -0
- data/sig/docrb/parser.rbs +5 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bfa4a19237733b6e0ace1d9af8584eeeb1dc671ec3a08c000d2b5f99adfcfa90
|
4
|
+
data.tar.gz: 02e8182ff82e529c89c99655411a7980a56c89a511263923616468c1bbbc0f65
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '048771cd5044806674962f019c76fa4fedbd5f2778ff461b3b7fd4eef77076bf93b4562283b5667318d0f68d99d7c816dd77b6a803c26fe8859f6f8358075ea7'
|
7
|
+
data.tar.gz: ba979ca0cb853ec2a3ef2542957f887f0b5b9e92355e380e1006bfd6883e06506b65ed49c3ed867116f7f11bfc9e32470b595a140e2917d669f5b407667e579c
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.2
|
3
|
+
SuggestExtensions: false
|
4
|
+
NewCops: enable
|
5
|
+
Exclude:
|
6
|
+
- spec/fixtures/*.rb
|
7
|
+
|
8
|
+
Style/StringLiterals:
|
9
|
+
Enabled: true
|
10
|
+
EnforcedStyle: double_quotes
|
11
|
+
|
12
|
+
Style/StringLiteralsInInterpolation:
|
13
|
+
Enabled: true
|
14
|
+
EnforcedStyle: double_quotes
|
15
|
+
|
16
|
+
Layout/LineLength:
|
17
|
+
Max: 120
|
18
|
+
Exclude:
|
19
|
+
- spec/**/**
|
20
|
+
|
21
|
+
Naming/VariableNumber:
|
22
|
+
Exclude:
|
23
|
+
- spec/**/**
|
24
|
+
|
25
|
+
Layout/FirstHashElementIndentation:
|
26
|
+
EnforcedStyle: consistent
|
27
|
+
|
28
|
+
Layout/EndAlignment:
|
29
|
+
EnforcedStyleAlignWith: start_of_line
|
30
|
+
|
31
|
+
Layout/MultilineMethodCallIndentation:
|
32
|
+
EnforcedStyle: indented
|
33
|
+
|
34
|
+
Style/Documentation:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
Layout/CaseIndentation:
|
38
|
+
EnforcedStyle: end
|
39
|
+
|
40
|
+
Layout/FirstArgumentIndentation:
|
41
|
+
EnforcedStyle: consistent_relative_to_receiver
|
42
|
+
|
43
|
+
Layout/ArgumentAlignment:
|
44
|
+
EnforcedStyle: with_fixed_indentation
|
45
|
+
|
46
|
+
Style/EmptyCaseCondition:
|
47
|
+
Enabled: false
|
48
|
+
|
49
|
+
Metrics/BlockLength:
|
50
|
+
Enabled: false
|
51
|
+
|
52
|
+
Metrics/ClassLength:
|
53
|
+
Enabled: false
|
54
|
+
|
55
|
+
Metrics/ModuleLength:
|
56
|
+
Enabled: false
|
57
|
+
|
58
|
+
Metrics/MethodLength:
|
59
|
+
Enabled: false
|
60
|
+
|
61
|
+
Metrics/AbcSize:
|
62
|
+
Enabled: false
|
63
|
+
|
64
|
+
Metrics/CyclomaticComplexity:
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
Metrics/PerceivedComplexity:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
Naming/PredicateName:
|
71
|
+
Enabled: false
|
72
|
+
|
73
|
+
Naming/FileName:
|
74
|
+
Exclude:
|
75
|
+
- lib/docrb-parser.rb
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/docrb/parser/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "docrb-parser"
|
7
|
+
spec.version = Docrb::Parser::VERSION
|
8
|
+
spec.authors = ["Victor Gama"]
|
9
|
+
spec.email = ["hey@vito.io"]
|
10
|
+
|
11
|
+
spec.summary = "Docrb's Ruby Parser"
|
12
|
+
spec.description = <<~DESC
|
13
|
+
docrb-parser is responsible for parsing Ruby sources into a structured#{" "}
|
14
|
+
format for usage by docrb and docrb-html.
|
15
|
+
DESC
|
16
|
+
spec.homepage = "https://github.com/heyvito/docrb"
|
17
|
+
spec.license = "MIT"
|
18
|
+
spec.required_ruby_version = ">= 3.2"
|
19
|
+
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
21
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/trunk/lib/docrb-parser"
|
22
|
+
spec.metadata["changelog_uri"] = spec.homepage
|
23
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
24
|
+
|
25
|
+
# Specify which files should be added to the gem when it is released.
|
26
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
27
|
+
spec.files = Dir.chdir(__dir__) do
|
28
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
29
|
+
(File.expand_path(f) == __FILE__) ||
|
30
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
spec.bindir = "exe"
|
34
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
35
|
+
spec.require_paths = ["lib"]
|
36
|
+
|
37
|
+
spec.add_dependency "prism", "~> 0.13"
|
38
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Object
|
4
|
+
def own_methods = methods.sort - Object.methods
|
5
|
+
def object_id_hex = "0x#{object_id.to_s(16).rjust(16, "0")}"
|
6
|
+
|
7
|
+
def self.docrb_inspect(&)
|
8
|
+
return if @__inspect__installed__
|
9
|
+
|
10
|
+
@__inspect__installed__ = true
|
11
|
+
define_method(:to_s) { "<#{self.class.name}:#{object_id_hex} #{instance_exec(&)}>" }
|
12
|
+
define_method(:inspect) { to_s }
|
13
|
+
end
|
14
|
+
|
15
|
+
def try(method, *, **, &)
|
16
|
+
return nil unless respond_to? method
|
17
|
+
|
18
|
+
send(method, *, **, &)
|
19
|
+
end
|
20
|
+
|
21
|
+
def attr_list(*names)
|
22
|
+
names.map { "#{_1}: #{send(_1).inspect}" }.join(", ")
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.docrb_inspect_attrs(*)
|
26
|
+
@inspectable_attrs = superclass.instance_variable_get(:@inspectable_attrs).dup || [] if @inspectable_attrs.nil?
|
27
|
+
@inspectable_attrs.append(*)
|
28
|
+
docrb_inspect { attr_list(*self.class.instance_variable_get(:@inspectable_attrs)) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.visible_attr_reader(*)
|
32
|
+
attr_reader(*)
|
33
|
+
|
34
|
+
docrb_inspect_attrs(*)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.visible_attr_accessor(*)
|
38
|
+
attr_accessor(*)
|
39
|
+
|
40
|
+
docrb_inspect_attrs(*)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Array
|
45
|
+
def first!
|
46
|
+
first or raise("#first! called on empty array")
|
47
|
+
end
|
48
|
+
|
49
|
+
alias old_first first
|
50
|
+
|
51
|
+
def first(*, **, &)
|
52
|
+
return old_first(*, **) unless block_given?
|
53
|
+
|
54
|
+
lazy.map(&).filter(&:itself).first
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module Kernel
|
59
|
+
def then! = nil? ? nil : yield(self)
|
60
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class Parser
|
5
|
+
class Attribute
|
6
|
+
visible_attr_reader :name, :location
|
7
|
+
visible_attr_accessor :writer_visibility, :reader_visibility, :type
|
8
|
+
attr_accessor :parent, :doc
|
9
|
+
|
10
|
+
def initialize(parser, parent, node, name, type)
|
11
|
+
@object_id = parser.make_id(self)
|
12
|
+
@name = name
|
13
|
+
@parent = parent
|
14
|
+
@location = parser.location(node.location)
|
15
|
+
@type = type
|
16
|
+
(parent.current_visibility_modifier || :public).tap do |vis|
|
17
|
+
@writer_visibility = vis
|
18
|
+
@reader_visibility = vis
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def id = @object_id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class Parser
|
5
|
+
class Call
|
6
|
+
visible_attr_reader :name, :arguments, :parent, :location
|
7
|
+
|
8
|
+
def initialize(parser, parent, node)
|
9
|
+
@object_id = parser.make_id(self)
|
10
|
+
@name = node.name.to_sym
|
11
|
+
@arguments = []
|
12
|
+
@parent = parent
|
13
|
+
@location = parser.location(node.location)
|
14
|
+
node.arguments&.arguments&.each do |arg|
|
15
|
+
@arguments << case arg.type
|
16
|
+
when :constant_path_node, :constant_read_node
|
17
|
+
parser.unfurl_constant_path(arg)
|
18
|
+
else
|
19
|
+
arg
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def id = @object_id
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class Parser
|
5
|
+
class Class < Container
|
6
|
+
visible_attr_accessor :inherits, :singleton
|
7
|
+
attr_accessor :node
|
8
|
+
|
9
|
+
def kind = :class
|
10
|
+
|
11
|
+
def initialize(parser, parent, node)
|
12
|
+
@default_constructor_visibility = :public
|
13
|
+
|
14
|
+
# WARNING: super WILL CALL methods that may require ivars to already be
|
15
|
+
# defined. Define those ivars before this point.
|
16
|
+
super
|
17
|
+
|
18
|
+
@inherits = if node&.type == :class_node && !node.superclass.nil?
|
19
|
+
reference(parser.unfurl_constant_path(node.superclass))
|
20
|
+
end
|
21
|
+
|
22
|
+
update_constructor_visibility!
|
23
|
+
adjust_split_attributes! :class
|
24
|
+
adjust_split_attributes! :instance
|
25
|
+
end
|
26
|
+
|
27
|
+
def unowned_classes
|
28
|
+
super.tap do |arr|
|
29
|
+
arr.merge_unowned(*@inherits.dereference!.all_classes) if @inherits&.fulfilled?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def unowned_modules
|
34
|
+
super.tap do |arr|
|
35
|
+
arr.merge_unowned(*@inherits.dereference!.unowned_modules) if @inherits&.fulfilled?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def unowned_instance_methods
|
40
|
+
super.tap do |arr|
|
41
|
+
arr.merge_unowned(*@inherits.dereference!.all_instance_methods) if @inherits&.fulfilled?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def unowned_class_methods
|
46
|
+
super.tap do |arr|
|
47
|
+
arr.merge_unowned(*@inherits.dereference!.unowned_class_methods) if @inherits&.fulfilled?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def unowned_class_attributes
|
52
|
+
super.tap do |arr|
|
53
|
+
arr.merge_unowned(*@inherits.dereference!.unowned_class_attributes) if @inherits&.fulfilled?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def unowned_instance_attributes
|
58
|
+
super.tap do |arr|
|
59
|
+
arr.merge_unowned(*@inherits.dereference!.unowned_instance_attributes) if @inherits&.fulfilled?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_inherited?(obj, parent)
|
64
|
+
return false if parent.nil? || !parent.fulfilled?
|
65
|
+
return true if parent.resolved.id == obj.parent.id
|
66
|
+
|
67
|
+
is_inherited?(obj, parent.dereference!.inherits)
|
68
|
+
end
|
69
|
+
|
70
|
+
def source_of(obj)
|
71
|
+
return :inherited if is_inherited?(obj, inherits)
|
72
|
+
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
def singleton! = tap { @singleton = true }
|
77
|
+
|
78
|
+
def singleton? = @singleton || false
|
79
|
+
|
80
|
+
def handle_parsed_node(parser, node) = parser.unhandled_node! node
|
81
|
+
|
82
|
+
def merge_singleton_class(other)
|
83
|
+
raise ArgumentError, "Cannot merge non-singleton class #{other.name} into #{name}" unless other.singleton?
|
84
|
+
|
85
|
+
class_methods.append(*other.instance_methods)
|
86
|
+
class_attributes.append(*other.instance_attributes)
|
87
|
+
return if other.location.try(:virtual?)
|
88
|
+
|
89
|
+
@defined_by << other.location
|
90
|
+
@location = other.location if @location.nil? || @location.try(:virtual)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class Parser
|
5
|
+
class Comment
|
6
|
+
attr_reader :comments
|
7
|
+
|
8
|
+
def initialize(parser, location)
|
9
|
+
@location = location
|
10
|
+
@file_path = location.file_path
|
11
|
+
@parser = parser
|
12
|
+
@comments = nil
|
13
|
+
locate
|
14
|
+
end
|
15
|
+
|
16
|
+
def locate
|
17
|
+
return if @location.virtual?
|
18
|
+
|
19
|
+
lines = @parser.lines_for(@file_path, @location.ast)
|
20
|
+
|
21
|
+
# NOTE: Regarding -2, -1 since `lines` is zero-indexed, and another -1
|
22
|
+
# to get the line before the current location
|
23
|
+
offset = @location.line_start - 2
|
24
|
+
comments = []
|
25
|
+
|
26
|
+
until offset.zero?
|
27
|
+
line = lines[offset]
|
28
|
+
break if line.nil?
|
29
|
+
|
30
|
+
break unless line.strip.start_with? "#"
|
31
|
+
|
32
|
+
comments << line
|
33
|
+
offset -= 1
|
34
|
+
end
|
35
|
+
|
36
|
+
@comments = comments.reverse.map(&:strip)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,290 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Docrb
|
4
|
+
class Parser
|
5
|
+
# CommentParser implements a small parser for matching comment's contents to
|
6
|
+
# relevant references and annotations.
|
7
|
+
class CommentParser
|
8
|
+
NEWLINE = "\n"
|
9
|
+
POUND = "#"
|
10
|
+
SPACE = " "
|
11
|
+
DASH = "-"
|
12
|
+
COLON = ":"
|
13
|
+
|
14
|
+
attr_accessor :objects, :current_object, :cursor, :visibility
|
15
|
+
|
16
|
+
def self.parse(data)
|
17
|
+
new(data)
|
18
|
+
.tap(&:parse)
|
19
|
+
.then do |parser|
|
20
|
+
{ meta: { visibility: parser.visibility }.compact, value: parser.objects }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(data)
|
25
|
+
@objects = []
|
26
|
+
@current_object = []
|
27
|
+
@data = data
|
28
|
+
.split(NEWLINE)
|
29
|
+
.map(&:rstrip)
|
30
|
+
.map { _1.gsub(/^\s*#\s?/, "") }
|
31
|
+
.join(NEWLINE)
|
32
|
+
.each_grapheme_cluster
|
33
|
+
.to_a
|
34
|
+
@data_len = @data.length
|
35
|
+
@visibility = nil
|
36
|
+
|
37
|
+
@cursor = 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def at_end? = (cursor >= @data_len)
|
41
|
+
|
42
|
+
def will_end? = (cursor + 1 >= @data_len)
|
43
|
+
|
44
|
+
def at_start? = cursor.zero?
|
45
|
+
|
46
|
+
def peek = at_end? ? nil : @data[cursor]
|
47
|
+
|
48
|
+
def peek_next = will_end? ? nil : @data[cursor + 1]
|
49
|
+
|
50
|
+
def peek_prev = at_start? ? nil : @data[cursor - 1]
|
51
|
+
|
52
|
+
def advance = at_end? ? nil : peek.tap { self.cursor += 1 }
|
53
|
+
|
54
|
+
def match?(*args) = args.any? { _1 == peek }
|
55
|
+
|
56
|
+
def consume_spaces = (advance while match?(SPACE) && !at_end?)
|
57
|
+
|
58
|
+
def extract_while = (current_object << advance while yield && !at_end?)
|
59
|
+
|
60
|
+
def extract_until
|
61
|
+
until at_end?
|
62
|
+
break if yield
|
63
|
+
|
64
|
+
current_object << advance
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse
|
69
|
+
parse_one until at_end?
|
70
|
+
flush_current_object
|
71
|
+
detect_field_list
|
72
|
+
process_code_examples
|
73
|
+
process_text_blocks
|
74
|
+
process_visibility
|
75
|
+
objects.map! { normalize_tree(_1) }
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def flush_current_object
|
80
|
+
data = current_object.join.rstrip
|
81
|
+
return if data.empty?
|
82
|
+
|
83
|
+
objects << data
|
84
|
+
current_object.clear
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_one
|
88
|
+
extract_until { match? NEWLINE }
|
89
|
+
advance # Consume newline
|
90
|
+
if match? NEWLINE
|
91
|
+
advance # consume newline
|
92
|
+
flush_current_object
|
93
|
+
return
|
94
|
+
end
|
95
|
+
current_object << peek_prev if peek_prev == NEWLINE
|
96
|
+
end
|
97
|
+
|
98
|
+
FIELD_LIST_HEADING = /^([a-z][a-z_0-9]*:?)\s+-\s+(.*)/
|
99
|
+
|
100
|
+
def detect_field_list
|
101
|
+
objects.each.with_index do |obj, idx|
|
102
|
+
definitions = obj.split("\n").reject { _1.start_with? SPACE }
|
103
|
+
|
104
|
+
if (definitions.length == 1 && definitions.first =~ FIELD_LIST_HEADING) ||
|
105
|
+
(definitions.first =~ FIELD_LIST_HEADING && definitions[1] =~ FIELD_LIST_HEADING)
|
106
|
+
return process_field_list(obj, idx)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_field_list(obj, at)
|
112
|
+
lines = obj.lines
|
113
|
+
result = {}
|
114
|
+
last_key = nil
|
115
|
+
lines.each do |line|
|
116
|
+
if (match = FIELD_LIST_HEADING.match(line))
|
117
|
+
last_key = match[1]
|
118
|
+
contents = match[2]
|
119
|
+
result[last_key] = contents
|
120
|
+
elsif last_key
|
121
|
+
result[last_key] = "#{result[last_key]} #{line.lstrip}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
objects[at] = { type: :fields, value: result }
|
125
|
+
end
|
126
|
+
|
127
|
+
def process_text_blocks
|
128
|
+
objects.each.with_index do |obj, idx|
|
129
|
+
next objects[idx] = process_text_block(obj) if obj.is_a? String
|
130
|
+
|
131
|
+
case obj[:type]
|
132
|
+
when :fields
|
133
|
+
obj[:value].transform_values! { process_text_block(_1) }
|
134
|
+
when :code_example then next
|
135
|
+
else
|
136
|
+
raise NotImplementedError, "Can't process text block for type #{obj[:type]}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def span(text) = { type: :span, value: text }
|
142
|
+
|
143
|
+
def process_text_block(text)
|
144
|
+
objs = [span(text)]
|
145
|
+
changed = true
|
146
|
+
while changed
|
147
|
+
changed = false
|
148
|
+
objs.each.with_index do |obj, idx|
|
149
|
+
next unless obj[:type] == :span
|
150
|
+
|
151
|
+
value = obj[:value]
|
152
|
+
changes = extract_method_reference(value) ||
|
153
|
+
extract_symbol(value) ||
|
154
|
+
extract_camelcase_identifier(value)
|
155
|
+
next unless changes
|
156
|
+
|
157
|
+
changes => { start_idx:, end_idx:, object: }
|
158
|
+
objs.delete_at(idx)
|
159
|
+
left = value[0...start_idx]
|
160
|
+
right = value[end_idx...]
|
161
|
+
|
162
|
+
new_items = [
|
163
|
+
(span(left) unless left.empty?),
|
164
|
+
object,
|
165
|
+
(span(right) unless right.empty?)
|
166
|
+
]
|
167
|
+
objs.insert(idx, *new_items.compact)
|
168
|
+
changed = true
|
169
|
+
break
|
170
|
+
end
|
171
|
+
end
|
172
|
+
objs.length == 1 ? objs.first : objs
|
173
|
+
end
|
174
|
+
|
175
|
+
# rubocop:disable Layout/LineLength
|
176
|
+
COMMENT_METHOD_REF_REGEXP = /(?:([A-Z][a-zA-Z0-9_]*::)*([A-Z][a-zA-Z0-9_]*))?(::|\.|#)([A-Za-z_][a-zA-Z0-9_@]*[!?]?)(?:\([a-zA-Z0-9=_,\s*]+\))?/
|
177
|
+
# rubocop:enable Layout/LineLength
|
178
|
+
|
179
|
+
def extract_method_reference(text)
|
180
|
+
match = COMMENT_METHOD_REF_REGEXP.match(text) or return nil
|
181
|
+
value, class_path, target, invocation, name = match.to_a
|
182
|
+
class_path&.gsub!(/::$/, "")
|
183
|
+
|
184
|
+
{
|
185
|
+
start_idx: match.begin(0),
|
186
|
+
end_idx: match.end(0),
|
187
|
+
object: {
|
188
|
+
type: invocation == POUND ? :method_ref : :class_path_ref,
|
189
|
+
class_path:,
|
190
|
+
target:,
|
191
|
+
name:,
|
192
|
+
value:
|
193
|
+
}
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
COMMENT_SYMBOL_REGEXP = /:(!|[@$][a-z_][a-z0-9_]*|[a-z_][a-z0-9_]*|[a-z_][a-z0-9_]*[?!]?)/i
|
198
|
+
|
199
|
+
def extract_symbol(text)
|
200
|
+
match = COMMENT_SYMBOL_REGEXP.match(text) or return nil
|
201
|
+
|
202
|
+
{
|
203
|
+
start_idx: match.begin(0),
|
204
|
+
end_idx: match.end(0),
|
205
|
+
object: {
|
206
|
+
type: :symbol,
|
207
|
+
value: match[0]
|
208
|
+
}
|
209
|
+
}
|
210
|
+
end
|
211
|
+
|
212
|
+
CAMELCASE_IDENTIFIER_REGEXP = /[A-Z][a-z]+(?:[A-Z][a-z]+)+/
|
213
|
+
|
214
|
+
def extract_camelcase_identifier(text)
|
215
|
+
match = CAMELCASE_IDENTIFIER_REGEXP.match(text) or return nil
|
216
|
+
|
217
|
+
{
|
218
|
+
start_idx: match.begin(0),
|
219
|
+
end_idx: match.end(0),
|
220
|
+
object: {
|
221
|
+
type: :identifier,
|
222
|
+
value: match[0]
|
223
|
+
}
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
VISIBILITY_INDICATOR_REGEXP = /^\s*(public|private|internal|deprecated|protected):\s+/i
|
228
|
+
|
229
|
+
def process_visibility(obj = nil)
|
230
|
+
obj ||= objects.first
|
231
|
+
case obj
|
232
|
+
when Array then process_visibility(obj.first)
|
233
|
+
when Hash
|
234
|
+
return if obj[:type] == :fields
|
235
|
+
return process_visibility(obj[:value]) unless obj[:type] == :span
|
236
|
+
|
237
|
+
value = obj[:value]
|
238
|
+
match = VISIBILITY_INDICATOR_REGEXP.match(value) or return nil
|
239
|
+
obj[:value] = value[match.end(0)...]
|
240
|
+
@visibility = match[1]
|
241
|
+
nil
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def process_code_examples
|
246
|
+
changed = true
|
247
|
+
while changed
|
248
|
+
start_at = nil
|
249
|
+
changed = false
|
250
|
+
objects.each.with_index do |obj, idx|
|
251
|
+
is_code = (obj.is_a?(String) && obj.start_with?(" "))
|
252
|
+
next start_at = idx if is_code && start_at.nil?
|
253
|
+
|
254
|
+
if !is_code && start_at
|
255
|
+
join_code_example_lines(start_at, idx)
|
256
|
+
start_at = nil
|
257
|
+
changed = true
|
258
|
+
break
|
259
|
+
end
|
260
|
+
end
|
261
|
+
join_code_example_lines(start_at, objects.length) unless start_at.nil?
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def join_code_example_lines(start_at, end_at)
|
266
|
+
lines = objects[start_at...end_at]
|
267
|
+
.map { _1.split("\n") }
|
268
|
+
.map { |el| el.map { "#{_1[2...]}\n" } }
|
269
|
+
.flatten
|
270
|
+
objects.slice!(start_at...end_at)
|
271
|
+
objects.insert(start_at, {
|
272
|
+
type: :code_example,
|
273
|
+
source: lines.join("\n")
|
274
|
+
})
|
275
|
+
end
|
276
|
+
|
277
|
+
def normalize_tree(obj)
|
278
|
+
if obj.is_a?(Array)
|
279
|
+
{ type: :block, value: obj }
|
280
|
+
elsif obj.is_a?(Hash) && obj[:type] == :span
|
281
|
+
{ type: :block, value: [obj] }
|
282
|
+
elsif obj.is_a?(Hash) && obj[:type] == :fields
|
283
|
+
obj.tap { |f| f[:value].transform_values! { normalize_tree(_1) } }
|
284
|
+
else
|
285
|
+
obj
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|