yard-virtus2 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.idea/modules.xml +8 -0
  4. data/Gemfile +17 -0
  5. data/Gemfile.lock +75 -0
  6. data/Guardfile +11 -0
  7. data/LICENSE.txt +19 -0
  8. data/README.md +61 -0
  9. data/Rakefile +9 -0
  10. data/example/README.md +4 -0
  11. data/example/address.rb +7 -0
  12. data/example/city.rb +5 -0
  13. data/example/user.rb +9 -0
  14. data/lib/yard-virtus.rb +18 -0
  15. data/lib/yard/virtus/code_objects.rb +2 -0
  16. data/lib/yard/virtus/code_objects/attribute_reader.rb +26 -0
  17. data/lib/yard/virtus/code_objects/attribute_writer.rb +48 -0
  18. data/lib/yard/virtus/declarations.rb +4 -0
  19. data/lib/yard/virtus/declarations/options.rb +53 -0
  20. data/lib/yard/virtus/declarations/type.rb +71 -0
  21. data/lib/yard/virtus/declarations/virtus_attribute.rb +73 -0
  22. data/lib/yard/virtus/declarations/virtus_model.rb +40 -0
  23. data/lib/yard/virtus/handlers.rb +2 -0
  24. data/lib/yard/virtus/handlers/include_virtus_model.rb +32 -0
  25. data/lib/yard/virtus/handlers/virtus_attribute.rb +58 -0
  26. data/lib/yard/virtus/mixin_handler_monkey_patch.rb +25 -0
  27. data/lib/yard/virtus/version.rb +5 -0
  28. data/spec/code_objects/attribute_reader_spec.rb +38 -0
  29. data/spec/code_objects/attribute_writer_spec.rb +75 -0
  30. data/spec/declarations/options_spec.rb +50 -0
  31. data/spec/declarations/type_spec.rb +37 -0
  32. data/spec/declarations/virtus_attribute_spec.rb +73 -0
  33. data/spec/declarations/virtus_model_spec.rb +35 -0
  34. data/spec/examples/include_virtus_model_001.rb.txt +15 -0
  35. data/spec/examples/virtus_attribute_001.rb.txt +65 -0
  36. data/spec/handlers/include_virtus_model_spec.rb +22 -0
  37. data/spec/handlers/virtus_attribute_spec.rb +67 -0
  38. data/spec/spec_helper.rb +65 -0
  39. data/spec/support/helpers/handler_helpers.rb +9 -0
  40. data/spec/support/helpers/parsing_helpers.rb +13 -0
  41. data/spec/support/matchers/attribute_matchers.rb +39 -0
  42. data/spec/support/matchers/method_type_matchers.rb +11 -0
  43. data/yard-virtus2.gemspec +22 -0
  44. metadata +119 -0
@@ -0,0 +1,73 @@
1
+ module YARD
2
+ module Virtus
3
+ module Declarations
4
+ # VirtusModel declaration wraps AST which represents
5
+ # call to `attribute` method.
6
+ # It's job is to provide information which documents attribute.
7
+ class VirtusAttribute
8
+ attr_reader :ast
9
+
10
+ # @param [YARD::Parser::Ruby::MethodCallNode] ast
11
+ def initialize(ast)
12
+ @ast = ast
13
+ @options = Options.new(parameters[2])
14
+ end
15
+
16
+ def readable?
17
+ true
18
+ end
19
+
20
+ def writable?
21
+ true
22
+ end
23
+
24
+ # Predicate to check if attribute has private writer.
25
+ def has_private_writer?
26
+ options[:writer] == :private
27
+ end
28
+
29
+ # Name of the attribute.
30
+ # @return [Symbol]
31
+ def attr_name
32
+ parameters.first.jump(:ident).first.to_sym
33
+ end
34
+
35
+ # Type of the attribute in YARD format.
36
+ # @return [String]
37
+ def type
38
+ Type.new(type_param).yard_type_string
39
+ end
40
+
41
+ def attribute_reader
42
+ CodeObjects::AttributeReader.new(attr_name, type)
43
+ end
44
+
45
+ def attribute_writer
46
+ CodeObjects::AttributeWriter.new(attr_name, type, has_private_writer?)
47
+ end
48
+
49
+ protected
50
+ attr_reader :options
51
+
52
+ def parameters
53
+ ast.parameters(false)
54
+ end
55
+
56
+ def type_param
57
+ parameters[1]
58
+ end
59
+
60
+ def scalar_type_to_string(ref)
61
+ ref.path.join("::")
62
+ end
63
+
64
+ def collection_type_to_string(type)
65
+ collection_type = scalar_type_to_string(type_param[0])
66
+ element_type = scalar_type_to_string(type_param[1].jump(:var_ref))
67
+
68
+ "#{collection_type}<#{element_type}>"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,40 @@
1
+ module YARD
2
+ module Virtus
3
+ module Declarations
4
+ # VirtusModel declaration wraps AST which represents mixin of Virtus.
5
+ # It's job is to provide information about Virtus features mixed-in
6
+ # via Virtus declaration.
7
+ #
8
+ # @example
9
+ # # This is AST for mixin source of `include Virtus.model`.
10
+ # ast = s(:call, s(:var_ref, s(:const, "Virtus")),
11
+ # :".",
12
+ # s(:ident, "model"))
13
+ #
14
+ # model = YARD::Virtus::Declarations::VirtusModel.new(ast)
15
+ # model.module_proxies_in_ns(ns) # => P("Virtus.model")
16
+ class VirtusModel
17
+ attr_reader :ast
18
+
19
+ # @param [YARD::Parser::Ruby::MethodCallNode] ast
20
+ def initialize(ast)
21
+ @ast = ast
22
+ end
23
+
24
+ # Get list of proxies to modules which document fetaures inherited
25
+ # via mixin.
26
+ #
27
+ # @param [YARD::CodeObjects::ClassObject] namespace
28
+ # @return [Array<YARD::CodeObjects::Proxy>]
29
+ def module_proxies_in_ns(namespace)
30
+ [YARD::CodeObjects::Proxy.new(namespace, mixin_name, :module)]
31
+ end
32
+
33
+ protected
34
+ def mixin_name
35
+ "%s.%s" % [ast.namespace, ast.method_name].map(&:source)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,2 @@
1
+ require File.join(File.dirname(__FILE__), "handlers", "include_virtus_model")
2
+ require File.join(File.dirname(__FILE__), "handlers", "virtus_attribute")
@@ -0,0 +1,32 @@
1
+ module YARD
2
+ module Virtus
3
+ module Handlers
4
+ class IncludeVirtusModel < YARD::Handlers::Ruby::Base
5
+ handles method_call(:include)
6
+ namespace_only
7
+
8
+ def process
9
+ raise YARD::Handlers::HandlerAborted unless virtus_module?
10
+
11
+ declaration = Declarations::VirtusModel.new(virtus_call)
12
+ declaration.module_proxies_in_ns(namespace).each do |proxy|
13
+ namespace.mixins(scope).unshift(proxy)
14
+ end
15
+
16
+ namespace[:supports_virtus_attributes] = true
17
+ end
18
+
19
+ protected
20
+
21
+ def virtus_module?
22
+ included_module = statement.parameters.jump(:var_ref)
23
+ included_module and included_module.source == "Virtus"
24
+ end
25
+
26
+ def virtus_call
27
+ statement.parameters(false)[0]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ module YARD
2
+ module Virtus
3
+ module Handlers
4
+ class VirtusAttribute < YARD::Handlers::Ruby::Base
5
+ handles method_call(:attribute)
6
+ namespace_only
7
+
8
+ def process
9
+ raise YARD::Handlers::HandlerAborted unless virtus_model?
10
+
11
+ declaration = Declarations::VirtusAttribute.new(statement)
12
+
13
+
14
+ register_attribute!(declaration.attribute_reader, :read) if declaration.readable?
15
+ register_attribute!(declaration.attribute_writer, :write) if declaration.writable?
16
+ end
17
+
18
+ protected
19
+
20
+ # @param [CodeObjects::AttributeReader, CodeObjects::AttributeWriter] mobject
21
+ # @param [YARD::CodeObjects::MethodObject] mobject
22
+ # @param [:read, :write] type
23
+ def register_attribute!(mobject, type)
24
+ yard_mobject = mobject.yard_method_object(namespace)
25
+
26
+ register_preserving_tags!(yard_mobject)
27
+ attributes_data_for(mobject.attr_name)[type] = yard_mobject
28
+ end
29
+
30
+ def attributes_data_for(name)
31
+ namespace.attributes[scope][name] ||= SymbolHash[:read => nil, :write => nil]
32
+ end
33
+
34
+ def virtus_model?
35
+ namespace.inheritance_tree(true).any? { |n| n[:supports_virtus_attributes] == true }
36
+ end
37
+
38
+ # When you register an object it can get assigned docstring which
39
+ # followed statement which was handled. If such a docstring exists it
40
+ # can result in removal of previously extracted tags like `@return` and
41
+ # `@private` as well as default values. To prevent it we restore all
42
+ # tags which disappeared after registration.
43
+ #
44
+ # Do you love uncontrolled mutations as much as I do?
45
+ def register_preserving_tags!(object)
46
+ tags_before_registration = object.tags
47
+
48
+ register(object)
49
+
50
+ lost_tags = tags_before_registration - object.tags
51
+ if lost_tags.size > 0
52
+ object.add_tag(*lost_tags)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # Standard YARD mixin handler can not process mixins which include method call.
2
+ # It throws UndocumentableError when it encounters any virtus mixin because they
3
+ # all include method call:
4
+ #
5
+ # [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class City
6
+ # [warn]: in file 'example/city.rb':2:
7
+ #
8
+ # 2: include Virtus.model
9
+ #
10
+ # This monkey patch aborts parsing of statement instead of raising UndocumentableError.
11
+ class YARD::Handlers::Ruby::MixinHandler < YARD::Handlers::Ruby::Base
12
+ protected
13
+ alias_method :original_process_mixin, :process_mixin
14
+
15
+ def process_mixin(mixin)
16
+ raise YARD::Handlers::HandlerAborted if virtus_module?(mixin)
17
+
18
+ original_process_mixin(mixin)
19
+ end
20
+
21
+ def virtus_module?(mixin)
22
+ included_module = mixin.jump(:var_ref)
23
+ included_module and included_module.source == "Virtus"
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module YARD
2
+ module Virtus
3
+ VERSION = "0.0.5"
4
+ end
5
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::CodeObjects::AttributeReader do
4
+ subject { described_class.new(:title, "String") }
5
+
6
+ it "has #attr_name" do
7
+ expect(subject.attr_name).to eq(:title)
8
+ end
9
+
10
+ it "has #type" do
11
+ expect(subject.type).to eq("String")
12
+ end
13
+
14
+ it "has #method_name" do
15
+ expect(subject.method_name).to eq(:title)
16
+ end
17
+
18
+ describe "#yard_method_object" do
19
+ let(:reader) { described_class.new(:title, "String") }
20
+ let(:namespace) { YARD::CodeObjects::ClassObject.new(nil, "TemporarySpecClass") }
21
+
22
+ subject { reader.yard_method_object(namespace) }
23
+
24
+ it { expect(subject).to be_instance_of(YARD::CodeObjects::MethodObject) }
25
+
26
+ it "is not explicit" do
27
+ expect(subject.is_explicit?).to be_false
28
+ end
29
+
30
+ it "has the same name as attribute it reads" do
31
+ expect(subject.name).to eq(:title)
32
+ end
33
+
34
+ it "has @return tag" do
35
+ expect(subject.has_tag?(:return)).to be_true
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,75 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::CodeObjects::AttributeWriter do
4
+ before :each do
5
+ # All YARD::CodeObjects::* objects are added to
6
+ # registry on creation which causes conflicts in
7
+ # this test.
8
+ YARD::Registry.clear
9
+ end
10
+
11
+ subject { described_class.new(:title, "String") }
12
+
13
+ it "has #attr_name" do
14
+ expect(subject.attr_name).to eq(:title)
15
+ end
16
+
17
+ it "has #type" do
18
+ expect(subject.type).to eq("String")
19
+ end
20
+
21
+ it "has #method_name" do
22
+ expect(subject.method_name).to eq(:"title=")
23
+ end
24
+
25
+ describe "#yard_method_object" do
26
+ let(:writer) { described_class.new(:title, "String") }
27
+ let(:namespace) { YARD::CodeObjects::ClassObject.new(nil, "TemporarySpecClass") }
28
+
29
+ subject { writer.yard_method_object(namespace) }
30
+
31
+ it { expect(subject).to be_instance_of(YARD::CodeObjects::MethodObject) }
32
+
33
+ it "is not explicit" do
34
+ expect(subject.is_explicit?).to be_false
35
+ end
36
+
37
+ it "has the writer name for attribute" do
38
+ expect(subject.name).to eq(:"title=")
39
+ end
40
+
41
+ it "has parameter" do
42
+ expect(subject.parameters).not_to be_empty
43
+ end
44
+
45
+ it "has type signature tag for parameter" do
46
+ param_tags = subject.tags(:param).select { |t| t.name == "value" }
47
+
48
+ expect(param_tags).not_to be_empty
49
+ end
50
+
51
+ context "when writer visibility is not specified" do
52
+ let(:writer) { described_class.new(:title, "String") }
53
+
54
+ it "does not have @private tag" do
55
+ expect(subject.tags(:private)).to be_empty
56
+ end
57
+ end
58
+
59
+ context "when writer is public" do
60
+ let(:writer) { described_class.new(:title, "String", false) }
61
+
62
+ it "does not have @private tag" do
63
+ expect(subject.tags(:private)).to be_empty
64
+ end
65
+ end
66
+
67
+ context "when writer is private" do
68
+ let(:writer) { described_class.new(:title, "String", true) }
69
+
70
+ it "has @private tag" do
71
+ expect(subject.tags(:private)).to have(1).item
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,50 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::Declarations::Options do
4
+ def self.example(src, &block)
5
+ c = context "'#{src}'"
6
+ # We can not generate AST for `:a => 1` so we wrap it into
7
+ # method call like in `attribute :a => 1` and then extract
8
+ # part corresponding to options. AST for wrapped code will look
9
+ # like this:
10
+ #
11
+ # s(:command,
12
+ # s(:ident, "attribute"),
13
+ # s(s(s(:assoc,
14
+ # s(:symbol_literal, s(:symbol, s(:ident, "a"))),
15
+ # s(:int, "1"))), false))
16
+ #
17
+ c.let(:ast) { ruby_ast("attribute #{src}")[1] }
18
+ c.class_eval(&block)
19
+ end
20
+
21
+ subject { described_class.new(ast) }
22
+
23
+ example "" do
24
+ it { expect(subject).to be_empty }
25
+ end
26
+
27
+ example ":a => :b" do
28
+ it { expect(subject).not_to be_empty }
29
+
30
+ it "has information about key :a" do
31
+ expect(subject[:a]).to eq(:b)
32
+ end
33
+ end
34
+
35
+ example "a: :b" do
36
+ it { expect(subject).not_to be_empty }
37
+
38
+ it "has information about key :a" do
39
+ expect(subject[:a]).to eq(:b)
40
+ end
41
+ end
42
+
43
+ example ":default => lambda { 123 }" do
44
+ it { expect(subject).not_to be_empty }
45
+
46
+ it "has information about lambda function" do
47
+ expect(subject[:default]).to be_kind_of(Proc)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe YARD::Virtus::Declarations::Type do
4
+ def self.example(src, &block)
5
+ c = context "'#{src}'"
6
+ c.let(:ast) { ruby_ast(src) }
7
+ c.class_eval(&block)
8
+ end
9
+
10
+ describe "#yard_type_string" do
11
+ let(:subject) { described_class.new(ast).yard_type_string }
12
+
13
+ example "String" do
14
+ it { expect(subject).to eq "String" }
15
+ end
16
+
17
+ example "Some::Nested::String" do
18
+ it { expect(subject).to eq "Some::Nested::String" }
19
+ end
20
+
21
+ example "Array[String]" do
22
+ it { expect(subject).to eq "Array<String>" }
23
+ end
24
+
25
+ example "Array[Nested::String]" do
26
+ it { expect(subject).to eq "Array<Nested::String>" }
27
+ end
28
+
29
+ example "Collection[Address]" do
30
+ it { expect(subject).to eq "Collection<Address>" }
31
+ end
32
+
33
+ example "Hash[String => Integer]" do
34
+ it { expect(subject).to eq "Hash{String => Integer}" }
35
+ end
36
+ end
37
+ end