diff --git a/.rubocop.yml b/.rubocop.yml index e3462a7..e6a0981 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,9 @@ AllCops: TargetRubyVersion: 2.6 + SuggestExtensions: false + NewCops: enable + Exclude: + - 'test/fixtures/**/*' Style/StringLiterals: Enabled: true diff --git a/Gemfile b/Gemfile index 4d7c0e2..cb5084c 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,9 @@ source "https://rubygems.org" # Specify your gem's dependencies in packwerk_yard.gemspec gemspec +# TODO: Move to gemspec after merged https://github.com/Shopify/packwerk/pull/375 +gem "packwerk", git: "https://github.com/richardmarbach/packwerk", branch: "configurable_parser_interface" + gem "rake", "~> 13.0" gem "minitest", "~> 5.16" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..4ad96f8 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,121 @@ +GIT + remote: https://github.com/richardmarbach/packwerk + revision: bf972227a1caaeed4d07b0302a7b74f20632fc06 + branch: configurable_parser_interface + specs: + packwerk (3.1.0) + activesupport (>= 6.0) + ast + better_html + bundler + constant_resolver (>= 0.2.0) + parallel + parser + sorbet-runtime (>= 0.5.9914) + zeitwerk (>= 2.6.1) + +PATH + remote: . + specs: + packwerk_yard (0.1.0) + yard + +GEM + remote: https://rubygems.org/ + specs: + actionview (7.1.2) + activesupport (= 7.1.2) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + ast (2.4.2) + base64 (0.2.0) + better_html (2.0.2) + actionview (>= 6.0) + activesupport (>= 6.0) + ast (~> 2.0) + erubi (~> 1.4) + parser (>= 2.4) + smart_properties + bigdecimal (3.1.5) + builder (3.2.4) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + constant_resolver (0.2.0) + crass (1.0.6) + drb (2.2.0) + ruby2_keywords + erubi (1.12.0) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + json (2.7.1) + language_server-protocol (3.17.0.3) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + minitest (5.20.0) + mutex_m (0.2.0) + nokogiri (1.16.0-arm64-darwin) + racc (~> 1.4) + parallel (1.24.0) + parser (3.2.2.4) + ast (~> 2.4.1) + racc + racc (1.7.3) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + rainbow (3.1.1) + rake (13.1.0) + regexp_parser (2.8.3) + rexml (3.2.6) + rubocop (1.59.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.4) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.30.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + smart_properties (1.17.0) + sorbet-runtime (0.5.11175) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) + yard (0.9.34) + zeitwerk (2.6.12) + +PLATFORMS + arm64-darwin-21 + +DEPENDENCIES + minitest (~> 5.16) + packwerk! + packwerk_yard! + rake (~> 13.0) + rubocop (~> 1.21) + +BUNDLED WITH + 2.4.18 diff --git a/lib/packwerk_yard.rb b/lib/packwerk_yard.rb index 0371bcd..4470cde 100644 --- a/lib/packwerk_yard.rb +++ b/lib/packwerk_yard.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true +require "packwerk" +require "yard" + require_relative "packwerk_yard/version" +require_relative "packwerk_yard/parser" -module PackwerkYard - class Error < StandardError; end - # Your code goes here... +module PackwerkYard # rubocop:disable Style/Documentation end diff --git a/lib/packwerk_yard/parser.rb b/lib/packwerk_yard/parser.rb new file mode 100644 index 0000000..70e0f36 --- /dev/null +++ b/lib/packwerk_yard/parser.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module PackwerkYard + class Parser # rubocop:disable Style/Documentation + include Packwerk::FileParser + + def initialize(ruby_parser: Packwerk::Parsers::Ruby.new) + @ruby_parser = ruby_parser + end + + def call(io:, file_path: "") + source_code = io.read + return to_ruby_ast(nil.inspect, file_path + ".yard") if source_code.nil? + + types = extract_from_yard_to_types(source_code) + + to_ruby_ast(types.map { |type| to_constant(type) }.compact.inspect, file_path + ".yard") + end + + def match?(path:) + path.end_with?(".rb") + end + + private + + def extract_from_yard_to_types(source_code) + # TODO: parallel で packwerk が動作すると競合が発生する可能性があるため調査する + YARD::Registry.clear + YARD::Parser::SourceParser.parse_string(source_code) + + types = YARD::Registry.all(:method).each_with_object([]) do |method_object, arr| + method_object.tags("param").each do |tag| + arr.concat(tag.types) if tag.types + end + + return_tag = method_object.tag("return") + arr.concat(return_tag.types) if return_tag&.types + end + + types.uniq + end + + def to_constant(name) + Object.const_get(name) + rescue NameError + nil + end + + def to_ruby_ast(code, file_path) + @ruby_parser.call( + io: StringIO.new(code), + file_path: file_path + ) + end + end +end diff --git a/packwerk_yard.gemspec b/packwerk_yard.gemspec index ffb5a70..87dbf7d 100644 --- a/packwerk_yard.gemspec +++ b/packwerk_yard.gemspec @@ -31,9 +31,6 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - # Uncomment to register a new dependency of your gem - # spec.add_dependency "example-gem", "~> 1.0" - - # For more information and examples about making a new gem, check out our - # guide at: https://bundler.io/guides/creating_gem.html + spec.add_dependency "yard" + spec.metadata["rubygems_mfa_required"] = "true" end diff --git a/test/fixtures/yard/not_exists_class_args.rb b/test/fixtures/yard/not_exists_class_args.rb new file mode 100644 index 0000000..602687d --- /dev/null +++ b/test/fixtures/yard/not_exists_class_args.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class NotExist + # @param [NotExistKlass] a + def not_exist_arg_type(a); end +end diff --git a/test/fixtures/yard/valid.rb b/test/fixtures/yard/valid.rb new file mode 100644 index 0000000..af81c17 --- /dev/null +++ b/test/fixtures/yard/valid.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class ValidYard + # @param [String] a + # @param [String] b + # @return [String] + def valid_method(a, b) + a + b + end +end diff --git a/test/test_packwerk_yard.rb b/test/test_packwerk_yard.rb deleted file mode 100644 index 27989ef..0000000 --- a/test/test_packwerk_yard.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class TestPackwerkYard < Minitest::Test - def test_that_it_has_a_version_number - refute_nil ::PackwerkYard::VERSION - end - - def test_it_does_something_useful - assert false - end -end diff --git a/test/unit/packwerk_yard/parser_test.rb b/test/unit/packwerk_yard/parser_test.rb new file mode 100644 index 0000000..32710ea --- /dev/null +++ b/test/unit/packwerk_yard/parser_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "test_helper" + +module PackwerkYard + class ParserTest < Minitest::Test + def test_call_returns_node_with_valid_file + parser = ::PackwerkYard::Parser.new + io = StringIO.new(File.read("test/fixtures/yard/valid.rb")) + node = parser.call(io: io, file_path: "test/fixtures/yard/valid.rb") + + assert_instance_of Parser::AST::Node, node + assert node.type, :array + assert node.children[0].children[1], :String + end + + def test_call_return_node_with_not_exists_class_args_file + parser = ::PackwerkYard::Parser.new + io = StringIO.new(File.read("test/fixtures/yard/not_exists_class_args.rb")) + node = parser.call(io: io, file_path: "test/fixtures/yard/not_exists_class_args.rb") + + assert_instance_of Parser::AST::Node, node + assert node.type, :array + assert node.children.size, 0 + end + end +end