From e2e68d9bd95fb3177d3f18ce616cc5bfd839d818 Mon Sep 17 00:00:00 2001 From: Paul Mannino Date: Fri, 15 Nov 2024 23:13:56 -0600 Subject: [PATCH] Implement InexactStruct DSL compiler --- .github/workflows/main.yml | 29 +++++ .rspec | 1 + .rubocop.yml | 15 +++ .ruby-version | 1 + Gemfile | 11 ++ Gemfile.lock | 111 ++++++++++++++++++++ LICENSE | 2 +- README.md | 0 inexact-struct-dsl-compiler.gemspec | 23 ++++ lib/inexact_struct_dsl_compiler.rb | 5 + lib/inexact_struct_dsl_compiler/version.rb | 5 + lib/tapioca/dsl/compilers/inexact_struct.rb | 40 +++++++ spec/fixtures/bar.rbi | 11 ++ spec/fixtures/baz.rbi | 20 ++++ spec/fixtures/foo.rbi | 11 ++ spec/inexact_struct_spec.rb | 61 +++++++++++ spec/spec_helper.rb | 7 ++ spec/support/dsl_command.rb | 27 +++++ 18 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/main.yml create mode 100644 .rspec create mode 100644 .rubocop.yml create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 README.md create mode 100644 inexact-struct-dsl-compiler.gemspec create mode 100644 lib/inexact_struct_dsl_compiler.rb create mode 100644 lib/inexact_struct_dsl_compiler/version.rb create mode 100644 lib/tapioca/dsl/compilers/inexact_struct.rb create mode 100644 spec/fixtures/bar.rbi create mode 100644 spec/fixtures/baz.rbi create mode 100644 spec/fixtures/foo.rbi create mode 100644 spec/inexact_struct_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/dsl_command.rb diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..0615f79 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['2.7', '3.0', '3.2', '3.3'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Lint + run: bundle exec rubocop + - name: Run tests + run: bundle exec rspec diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..50c3514 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,15 @@ +AllCops: + NewCops: enable + Exclude: + - 'spec/fixtures/**/*' + TargetRubyVersion: 3.0 + SuggestExtensions: false + +Metrics/MethodLength: + Enabled: false + +Metrics/BlockLength: + Enabled: false + +Style/Documentation: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..9c25013 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.6 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..5e899f7 --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +source 'http://rubygems.org' + +gemspec + +gem 'debug' +gem 'rspec' +gem 'rubocop' +gem 'sorbet-static-and-runtime' +gem 'tapioca' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..aba1e72 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,111 @@ +PATH + remote: . + specs: + inexact-struct-dsl-compiler (0.0.1) + tapioca (>= 0.13) + +GEM + remote: http://rubygems.org/ + specs: + ast (2.4.2) + debug (1.9.2) + irb (~> 1.10) + reline (>= 0.3.8) + diff-lcs (1.5.1) + erubi (1.13.0) + io-console (0.7.2) + irb (1.14.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.8.2) + language_server-protocol (3.17.0.3) + netrc (0.11.0) + parallel (1.26.3) + parser (3.3.6.0) + ast (~> 2.4.1) + racc + prism (1.2.0) + psych (5.2.0) + stringio + racc (1.8.1) + rainbow (3.1.1) + rbi (0.2.1) + prism (~> 1.0) + sorbet-runtime (>= 0.5.9204) + rdoc (6.7.0) + psych (>= 4.0.0) + regexp_parser (2.9.2) + reline (0.5.11) + io-console (~> 0.5) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) + rubocop (1.68.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.36.1) + parser (>= 3.3.1.0) + ruby-progressbar (1.13.0) + sorbet (0.5.11647) + sorbet-static (= 0.5.11647) + sorbet-runtime (0.5.11647) + sorbet-static (0.5.11647-aarch64-linux) + sorbet-static (0.5.11647-universal-darwin) + sorbet-static (0.5.11647-x86_64-linux) + sorbet-static-and-runtime (0.5.11647) + sorbet (= 0.5.11647) + sorbet-runtime (= 0.5.11647) + spoom (1.5.0) + erubi (>= 1.10.0) + prism (>= 0.28.0) + sorbet-static-and-runtime (>= 0.5.10187) + thor (>= 0.19.2) + stringio (3.1.2) + tapioca (0.16.4) + bundler (>= 2.2.25) + netrc (>= 0.11.0) + parallel (>= 1.21.0) + rbi (~> 0.2) + sorbet-static-and-runtime (>= 0.5.11087) + spoom (>= 1.2.0) + thor (>= 1.2.0) + yard-sorbet + thor (1.3.2) + unicode-display_width (2.6.0) + yard (0.9.37) + yard-sorbet (0.9.0) + sorbet-runtime + yard + +PLATFORMS + aarch64-linux + universal-darwin + x86_64-linux + +DEPENDENCIES + debug + inexact-struct-dsl-compiler! + rspec + rubocop + sorbet-static-and-runtime + tapioca + +BUNDLED WITH + 2.5.23 diff --git a/LICENSE b/LICENSE index b47aedb..8ed32bc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Paul +Copyright (c) 2024 Paul Mannino Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/inexact-struct-dsl-compiler.gemspec b/inexact-struct-dsl-compiler.gemspec new file mode 100644 index 0000000..a4d3131 --- /dev/null +++ b/inexact-struct-dsl-compiler.gemspec @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative 'lib/inexact_struct_dsl_compiler/version' + +Gem::Specification.new do |spec| + spec.name = 'inexact-struct-dsl-compiler' + spec.version = InexactStructDslCompiler::VERSION + spec.authors = ['Paul Mannino'] + spec.email = ['pmannino.code@gmail.com'] + spec.homepage = 'https://github.com/paul-mannino/inexact-struct-dsl-compiler' + spec.summary = 'A Tapioca DSL compiler for T::InexactStruct' + spec.description = 'A Tapioca DSL compiler that adds' + spec.license = 'MIT' + + spec.metadata['allowed_push_host'] = 'https://rubygems.org' + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = spec.homepage + + spec.required_ruby_version = '>= 3.0.0' + + spec.add_dependency 'tapioca', '>=0.13' + spec.metadata['rubygems_mfa_required'] = 'true' +end diff --git a/lib/inexact_struct_dsl_compiler.rb b/lib/inexact_struct_dsl_compiler.rb new file mode 100644 index 0000000..c4ff3a6 --- /dev/null +++ b/lib/inexact_struct_dsl_compiler.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require 'tapioca' + +module InexactStructDslCompiler; end diff --git a/lib/inexact_struct_dsl_compiler/version.rb b/lib/inexact_struct_dsl_compiler/version.rb new file mode 100644 index 0000000..b915075 --- /dev/null +++ b/lib/inexact_struct_dsl_compiler/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module InexactStructDslCompiler + VERSION = '0.0.1' +end diff --git a/lib/tapioca/dsl/compilers/inexact_struct.rb b/lib/tapioca/dsl/compilers/inexact_struct.rb new file mode 100644 index 0000000..09c8751 --- /dev/null +++ b/lib/tapioca/dsl/compilers/inexact_struct.rb @@ -0,0 +1,40 @@ +# typed: true +# frozen_string_literal: true + +module Tapioca + module Compilers + class InexactStruct < Tapioca::Dsl::Compiler + extend T::Sig + + ConstantType = type_member { { fixed: T.class_of(T::InexactStruct) } } + + sig { override.returns(T::Enumerable[Module]) } + def self.gather_constants + all_classes.select { |c| c < T::InexactStruct }.reject do |c| + c.name =~ /^(Tapioca|RBI|Spoom|T|YARDSorbet)::/ + end + end + + sig { override.void } + def decorate + root.create_path(constant) do |klass| + klass.create_method('initialize', parameters: build_parameters, return_type: 'void') + end + end + + sig {returns(T::Array[RBI::TypedParam])} + def build_parameters + constant.props.map do |name, prop| + type = prop.fetch(:type_object).to_s + if prop[:default] + create_kw_opt_param(name.to_s, type: type, default: 'T.unsafe(nil)') + elsif T::Props::Utils.optional_prop?(prop) + create_kw_opt_param(name.to_s, type: type, default: 'nil') + else + create_kw_param(name.to_s, type: type) + end + end + end + end + end +end diff --git a/spec/fixtures/bar.rbi b/spec/fixtures/bar.rbi new file mode 100644 index 0000000..5d38615 --- /dev/null +++ b/spec/fixtures/bar.rbi @@ -0,0 +1,11 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Bar`. +# Please instead update this file by running `bin/tapioca dsl Bar`. + + +class Bar + sig { params(r: ::Float, x: T::Boolean, y: T.nilable(::String), z: T.any(::Integer, T::Boolean)).void } + def initialize(r: T.unsafe(nil), x:, y: nil, z:); end +end diff --git a/spec/fixtures/baz.rbi b/spec/fixtures/baz.rbi new file mode 100644 index 0000000..5869ea4 --- /dev/null +++ b/spec/fixtures/baz.rbi @@ -0,0 +1,20 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Baz`. +# Please instead update this file by running `bin/tapioca dsl Baz`. + + +class Baz + sig do + params( + a: ::Integer, + b: ::String, + c: ::Bar, + c2: T.nilable(::Foo), + d: T::Hash[::String, ::String], + e: T.untyped + ).void + end + def initialize(a:, b:, c:, c2: T.unsafe(nil), d:, e:); end +end diff --git a/spec/fixtures/foo.rbi b/spec/fixtures/foo.rbi new file mode 100644 index 0000000..0d3f27a --- /dev/null +++ b/spec/fixtures/foo.rbi @@ -0,0 +1,11 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Foo`. +# Please instead update this file by running `bin/tapioca dsl Foo`. + + +class Foo + sig { params(r: ::Float).void } + def initialize(r: T.unsafe(nil)); end +end diff --git a/spec/inexact_struct_spec.rb b/spec/inexact_struct_spec.rb new file mode 100644 index 0000000..5cb7d39 --- /dev/null +++ b/spec/inexact_struct_spec.rb @@ -0,0 +1,61 @@ +# typed: false +# frozen_string_literal: true + +require 'spec_helper' +require 'tmpdir' + +describe 'Tapioca::Compilers::InexactStruct' do + # rubocop:disable Lint/ConstantDefinitionInBlock + class Foo < T::InexactStruct + const :r, Float, default: 1.0 + end + + class Bar < Foo + const :x, T::Boolean + const :y, T.nilable(String) + const :z, T.any(Integer, T::Boolean) + end + + class Baz < T::InexactStruct + const :a, Integer + const :b, String + const :c, Bar + const :c2, T.nilable(Foo), default: Foo.new + const :d, T::Hash[String, String] + const :e, T.untyped + end + # rubocop:enable Lint/ConstantDefinitionInBlock + + let(:tmp_dir) { Dir.mktmpdir } + + around(:each) do |example| + DslCommand.build( + requested_constants: requested_constants, + outpath: Pathname.new(tmp_dir) + ).run + example.run + ensure + FileUtils.remove_entry(tmp_dir) + end + + let(:requested_constants) { [] } + + it 'generates rbi files and they are identical to test data' do + expected = Dir['./spec/fixtures/*.rbi'] + actual = Dir[File.join(tmp_dir, '*.rbi')] + expect(expected.size).to eq(actual.size) + expected.zip(actual).each do |expected_example, actual_example| + expect(File.read(expected_example)).to(eq(File.read(actual_example))) + end + end + + context 'with requested_constants' do + let(:requested_constants) { ['Foo'] } + + it 'generates rbi files only for requested_constants' do + actual = Dir[File.join(tmp_dir, '*.rbi')] + expect(actual.size).to eq(1) + expect(actual.first).to match(/foo.rbi$/) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..f985802 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'tapioca' +require 'tapioca/internal' +require './spec/support/dsl_command' + +RSpec.configure diff --git a/spec/support/dsl_command.rb b/spec/support/dsl_command.rb new file mode 100644 index 0000000..b149e17 --- /dev/null +++ b/spec/support/dsl_command.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module DslCommand + def self.build(requested_constants:, outpath:) + rbi_formatter = ::Tapioca::DEFAULT_RBI_FORMATTER + rbi_formatter.max_line_length = 120 + + ::Tapioca::Commands::DslGenerate.new( + requested_constants: requested_constants, + requested_paths: [], + outpath: outpath, + only: [], + exclude: [], + file_header: true, + tapioca_path: ::Tapioca::TAPIOCA_DIR, + quiet: true, + verbose: false, + number_of_workers: 1, + auto_strictness: true, + gem_dir: ::Tapioca::DEFAULT_GEM_DIR, + rbi_formatter: rbi_formatter, + app_root: '.', + halt_upon_load_error: true, + compiler_options: {} + ) + end +end