docrb-parser 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|