rbs-patch 0.1.2 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fd64b165683dbb9350cc535cd07176d1e5b74fd3814b72b7d4e94ad9e8deafe
4
- data.tar.gz: 48b0f85c818af5f23bafd3d7b7acbe281b5f3c7ba8c26f270d04c1e1b910f9d3
3
+ metadata.gz: ef229a0a3efd1a66cdc57652ef80b173d19311652f376422a06bad6d61d3c7b5
4
+ data.tar.gz: 364bff243f157949a91b88c936a953e88cf275589312c664df1a9f6cc2d89c40
5
5
  SHA512:
6
- metadata.gz: 1548885b396ccff12ebe6ae6c1b5272133248623db7360378283b9f707f17462c1bd7e5d26a9107865277e23fad005f9f83b779cdca992943bc33dccefc3bad6
7
- data.tar.gz: 16da1d9c8b866dc6a4c6c0a6ed8713da52db81556a8e850f26badb6df0f25ba1fd883e81260f26764da667eea2fcb2dba0ceed4f975b7e4b71baeafe04ef5fdc
6
+ metadata.gz: ad22a7fbeff8115b185977f5d21bfdbd51b53485f1e1896dc0e692fccbf6e8de82b90f499e3702343dbb579798cbe86b4ad871926aa8089763d4e1599d9c29e2
7
+ data.tar.gz: 2850321317aa8b97317f315b4dce55af70330c724cd279dadbd5f8b0c8f9edc3e4305dbfaeca0aa85365123caa1772b4bb7137487df4b7fbf6c9b6d9ab66c7aa
data/README.md CHANGED
@@ -27,7 +27,160 @@ gem install rbs-patch
27
27
 
28
28
  ## Usage
29
29
 
30
- TODO: Write usage instructions here
30
+ ### Basic Usage
31
+
32
+ ```bash
33
+ # Apply patches to RBS files
34
+ rbs-patch base.rbs patch1.rbs patch2.rbs
35
+
36
+ # Mix files and directories
37
+ rbs-patch lib/types/ sig/patches/
38
+
39
+ # Output goes to stdout - redirect to save
40
+ rbs-patch base.rbs patch.rbs > output.rbs
41
+ ```
42
+
43
+ ### Programmatic Usage
44
+
45
+ ```ruby
46
+ require 'rbs/patch'
47
+
48
+ p = RBS::Patch.new
49
+
50
+ # Load from a single file
51
+ p.apply(path: Pathname("sig/user.rbs"))
52
+
53
+ # Load from a directory (all .rbs files)
54
+ p.apply(path: Pathname("sig/patches"))
55
+
56
+ # Apply from string
57
+ p.apply(<<~RBS)
58
+ class User
59
+ %a{patch:override}
60
+ def name: () -> String?
61
+ end
62
+ RBS
63
+
64
+ puts p.to_s
65
+ ```
66
+
67
+ ### Annotation Syntax
68
+
69
+ All patch operations use RBS annotations with the format `%a{patch:operation}` or `%a{patch:operation:target}`.
70
+
71
+ #### Method-level Operations
72
+
73
+ ##### `override` - Replace existing method signature
74
+
75
+ ```ruby
76
+ class User
77
+ %a{patch:override}
78
+ def name: () -> String? # Replaces existing method signature at the same position
79
+ end
80
+ ```
81
+
82
+ ##### `delete` - Remove method signature
83
+
84
+ ```ruby
85
+ class User
86
+ %a{patch:delete}
87
+ def email: () -> String # Removes this method from the class
88
+ end
89
+ ```
90
+
91
+ ##### `append_after(method_name)` - Insert method after specified method
92
+
93
+ ```ruby
94
+ class User
95
+ %a{patch:append_after(name)}
96
+ def nickname: () -> String? # Inserts after the 'name' method
97
+ end
98
+ ```
99
+
100
+ ##### `prepend_before(method_name)` - Insert method before specified method
101
+
102
+ ```ruby
103
+ class User
104
+ %a{patch:prepend_before(name)}
105
+ def id: () -> Integer # Inserts before the 'name' method
106
+ end
107
+ ```
108
+
109
+ #### Class/Module-level Operations
110
+
111
+ ##### `override` - Replace entire class/module
112
+
113
+ ```ruby
114
+ %a{patch:override}
115
+ class User
116
+ def name: () -> String # Completely replaces the User class definition
117
+ end
118
+ ```
119
+
120
+ ##### `delete` - Remove class/module
121
+
122
+ ```ruby
123
+ %a{patch:delete}
124
+ class User
125
+ end # Removes the entire User class
126
+ ```
127
+
128
+ ##### `append_after(ClassName)` - Insert class/module after specified class
129
+
130
+ ```ruby
131
+ %a{patch:append_after(User)}
132
+ class Admin
133
+ def permissions: () -> Array[String]
134
+ end # Inserts Admin class after User class
135
+ ```
136
+
137
+ ##### `prepend_before(ClassName)` - Insert class/module before specified class
138
+
139
+ ```ruby
140
+ %a{patch:prepend_before(User)}
141
+ class Guest
142
+ def readonly: () -> bool
143
+ end # Inserts Guest class before User class
144
+ ```
145
+
146
+ ### Working with Nested Modules
147
+
148
+ Operations work correctly within nested module structures:
149
+
150
+ ```ruby
151
+ module MyApp
152
+ module Models
153
+ %a{patch:append_after(User)}
154
+ class Admin
155
+ def role: () -> String
156
+ end
157
+ end
158
+ end
159
+ ```
160
+
161
+ ### Merging Multiple Definitions
162
+
163
+ Without annotations, multiple class definitions are merged:
164
+
165
+ ```ruby
166
+ p.apply(<<~RBS)
167
+ class User
168
+ def name: () -> String
169
+ end
170
+ RBS
171
+
172
+ p.apply(<<~RBS)
173
+ class User
174
+ def email: () -> String # Adds to existing User class
175
+ end
176
+ RBS
177
+
178
+ # Result:
179
+ # class User
180
+ # def name: () -> String
181
+ # def email: () -> String
182
+ # end
183
+ ```
31
184
 
32
185
  ## Development
33
186
 
data/Rakefile CHANGED
@@ -9,4 +9,10 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[test rubocop]
12
+ require "steep/rake_task"
13
+ Steep::RakeTask.new do |t|
14
+ t.check.severity_level = :error
15
+ t.watch.verbose
16
+ end
17
+
18
+ task default: %i[test rubocop steep]
data/Steepfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ target :lib do
4
+ signature "sig"
5
+
6
+ check "lib"
7
+ library "rbs"
8
+ end
data/exe/rbs-patch CHANGED
@@ -6,6 +6,8 @@ require "rbs/patch"
6
6
  require "pathname"
7
7
 
8
8
  if ARGV.empty?
9
+ puts "rbs-patch #{RBS::Patch::VERSION}"
10
+ puts ""
9
11
  puts "Usage: rbs-patch RBS_FILE_PATH [RBS_FILE_PATH ...]"
10
12
  puts ""
11
13
  puts "Examples:"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RBS
4
4
  class Patch
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.4"
6
6
  end
7
7
  end
data/lib/rbs/patch.rb CHANGED
@@ -8,11 +8,18 @@ module RBS
8
8
  class Patch # rubocop:disable Style/Documentation
9
9
  ANNOTATION_OVERRIDE = "patch:override"
10
10
  ANNOTATION_DELETE = "patch:delete"
11
- ANNOTATION_APPEND_AFTER = /\Apatch:append_after:(.*)\Z/
12
- ANNOTATION_PREPEND_BEFORE = /\Apatch:prepend_before:(.*)\Z/
11
+ ANNOTATION_APPEND_AFTER = /\Apatch:append_after\((.*)\)\Z/
12
+ ANNOTATION_PREPEND_BEFORE = /\Apatch:prepend_before\((.*)\)\Z/
13
13
 
14
14
  def initialize
15
- @env = ::RBS::Environment.new
15
+ @decls = []
16
+ end
17
+
18
+ def to_s
19
+ io = ::StringIO.new
20
+ ::RBS::Writer.new(out: io).write(@decls)
21
+ io.rewind
22
+ io.read || ""
16
23
  end
17
24
 
18
25
  def apply(source = nil, path: nil)
@@ -22,83 +29,153 @@ module RBS
22
29
  next if files.include?(path)
23
30
 
24
31
  files << path
25
- apply Buffer.new(name: path, content: path.read(encoding: "UTF-8"))
32
+ apply ::RBS::Buffer.new(name: path, content: path.read(encoding: "UTF-8"))
26
33
  end
27
34
  return
28
35
  end
29
36
 
30
- _, dirs, decls = ::RBS::Parser.parse_signature(source)
31
- @env.add_source(::RBS::Source::RBS.new(source, dirs, decls))
32
- @env.class_decls.each_value.map do |class_entry|
33
- class_entry.context_decls.map { _2 }.inject do |decl_a, decl_b|
34
- decl_b.members.delete_if do |member_b|
35
- ope, arg = if member_b.annotations.any? { |a| a.string == ANNOTATION_OVERRIDE }
36
- [:override, nil]
37
- elsif member_b.annotations.any? { |a| a.string == ANNOTATION_DELETE }
38
- [:delete, nil]
39
- elsif (anno = member_b.annotations.find { |a| a.string.match(ANNOTATION_APPEND_AFTER) })
40
- [:append_after, anno.string.match(ANNOTATION_APPEND_AFTER)[1]]
41
- elsif (anno = member_b.annotations.find { |a| a.string.match(ANNOTATION_PREPEND_BEFORE) })
42
- [:prepend_before, anno.string.match(ANNOTATION_PREPEND_BEFORE)[1]]
43
- end
44
-
45
- next unless ope
46
-
47
- case ope
48
- when :override
49
- index = decl_a.members.find_index { |member_a| member_a.name == member_b.name }
50
- if index
51
- decl_a.members[index] = decl_a.members[index].update(overloads: member_b.overloads)
52
- true
53
- else
54
- false
55
- end
56
- when :delete
57
- decl_a.members.reject! { |member_a| member_a.name == member_b.name }
58
- when :append_after, :prepend_before
59
- target_name = arg.to_sym
60
- index = decl_a.members.find_index { |member_a| member_a.name == target_name }
61
- if index
62
- if ope == :append_after
63
- offset = 1
64
- annotations = member_b.annotations.reject { |a| a.string.match(ANNOTATION_APPEND_AFTER) }
65
- else
66
- offset = 0
67
- annotations = member_b.annotations.reject { |a| a.string.match(ANNOTATION_PREPEND_BEFORE) }
68
- end
69
- decl_a.members.insert(index + offset, member_b.update(annotations:))
70
- true
71
- else
72
- false
73
- end
74
- end
75
- end
76
- decl_a
37
+ _, _, decls = ::RBS::Parser.parse_signature(source)
38
+ walk(decls) do |decl, name|
39
+ ope, arg = process_annotations(decl.annotations) if decl.respond_to?(:annotations) # steep:ignore
40
+
41
+ case ope
42
+ when :override
43
+ override(name, with: decl)
44
+ when :delete
45
+ delete(name)
46
+ when :append_after
47
+ add(decl, to: name, after: arg)
48
+ when :prepend_before
49
+ add(decl, to: name, before: arg)
50
+ else
51
+ add(decl, to: name)
77
52
  end
78
53
  end
79
54
  end
80
55
 
81
- def to_s
82
- decls = @env.class_decls.each_value.map do |class_entry|
83
- decls = class_entry.context_decls.map { _2 }
84
- decls.each_with_object(decls.first.update(members: [])) do |decl, new_decl|
85
- # merge multiple class decls into a single one
86
- new_decl.members.concat decl.members
87
- end
56
+ private
57
+
58
+ def extract_name(decl)
59
+ if decl.is_a?(::RBS::AST::Declarations::AliasDecl) # rubocop:disable Style/CaseLikeIf
60
+ decl.new_name.to_s
61
+ elsif decl.is_a?(::RBS::AST::Declarations::Base)
62
+ decl.name.to_s
63
+ elsif decl.is_a?(::RBS::AST::Members::LocationOnly)
64
+ ""
65
+ elsif decl.is_a?(::RBS::AST::Members::Alias) # rubocop:disable Lint/DuplicateBranch
66
+ decl.new_name.to_s
67
+ else # rubocop:disable Lint/DuplicateBranch
68
+ # ::RBS::AST::Members::t
69
+ decl.name.to_s
88
70
  end
71
+ end
72
+
73
+ def extract_members(decl)
74
+ decl.members if decl.is_a?(::RBS::AST::Declarations::NestedDeclarationHelper)
75
+ end
89
76
 
90
- classes = Set[]
77
+ def walk(decls, name_stack = [], &block)
91
78
  decls.each do |decl|
92
- decl.members.each do |member|
93
- classes << member.name if member.respond_to?(:name)
79
+ name_stack << extract_name(decl)
80
+ if decl.is_a?(::RBS::AST::Members::Base)
81
+ yield decl, "#{name_stack[..-2]&.join("::")}##{name_stack[-1]}"
82
+ else
83
+ yield decl, name_stack.join("::")
94
84
  end
85
+ members = extract_members(decl)
86
+ walk(members, name_stack, &block) if members
87
+ name_stack.pop
95
88
  end
96
- decls.delete_if { |c| classes.include?(c.name) }
89
+ end
97
90
 
98
- io = ::StringIO.new
99
- RBS::Writer.new(out: io).write(decls)
100
- io.rewind
101
- io.read
91
+ def decl_map
92
+ # @type var map: Hash[String, ::RBS::Patch::t]
93
+ map = {}
94
+ walk(@decls) { |decl, name| map[name] = decl }
95
+ map
96
+ end
97
+
98
+ def add(decl, to:, after: nil, before: nil)
99
+ map = decl_map
100
+ return if map.key?(to)
101
+
102
+ sep = decl.is_a?(::RBS::AST::Members::Base) ? "#" : "::"
103
+ namespace, = to.rpartition(sep)
104
+
105
+ target = namespace.empty? ? @decls : extract_members(map[namespace])
106
+
107
+ if target
108
+ decl.annotations.delete_if { |a| process_annotations([a]) } # steep:ignore
109
+ if after
110
+ index = target.find_index { |m| extract_name(m) == after }
111
+ if index
112
+ # steep:ignore:start
113
+ decl = decl.update(location: target[index].location.dup) if decl.respond_to?(:update) # rubocop:disable Style/RedundantSelfAssignment
114
+ # steep:ignore:end
115
+ target.insert(index + 1, decl)
116
+ end
117
+ elsif before
118
+ index = target.find_index { |m| extract_name(m) == before }
119
+ if index
120
+ # steep:ignore:start
121
+ decl = decl.update(location: target[index].location.dup) if decl.respond_to?(:update) # rubocop:disable Style/RedundantSelfAssignment
122
+ # steep:ignore:end
123
+ target.insert(index, decl)
124
+ end
125
+ else
126
+ target << decl
127
+ end
128
+ else
129
+ @decls << decl # steep:ignore
130
+ end
131
+ end
132
+
133
+ def override(name, with:)
134
+ map = decl_map
135
+ return unless map.key?(name)
136
+
137
+ sep = with.is_a?(::RBS::AST::Members::Base) ? "#" : "::"
138
+ namespace, _, name = name.rpartition(sep)
139
+
140
+ if namespace.empty?
141
+ # top level decl
142
+ index = @decls.find_index { |d| extract_name(d) == name }
143
+ @decls[index] = with # steep:ignore
144
+ else
145
+ members = extract_members(map[namespace])
146
+ index = members.find_index do |m| # steep:ignore
147
+ extract_name(m) == name
148
+ end
149
+ members[index] = with # steep:ignore
150
+ end
151
+ with.annotations.delete_if { |a| process_annotations([a]) } # steep:ignore
152
+ end
153
+
154
+ def delete(name)
155
+ map = decl_map
156
+ return unless map.key?(name)
157
+
158
+ sep = name.index("#") ? "#" : "::"
159
+ namespace, _, name = name.rpartition(sep)
160
+
161
+ if namespace.empty?
162
+ # top level decl
163
+ @decls.delete_if { |d| extract_name(d) == name }
164
+ else
165
+ extract_members(map[namespace])&.delete_if { |m| extract_name(m) == name }
166
+ end
167
+ end
168
+
169
+ def process_annotations(annotations) # steep:ignore
170
+ if annotations.any? { |a| a.string == ANNOTATION_OVERRIDE }
171
+ [:override, nil]
172
+ elsif annotations.any? { |a| a.string == ANNOTATION_DELETE }
173
+ [:delete, nil]
174
+ elsif (anno = annotations.find { |a| a.string.match(ANNOTATION_APPEND_AFTER) })
175
+ [:append_after, anno.string.match(ANNOTATION_APPEND_AFTER)&.[](1) || ""]
176
+ elsif (anno = annotations.find { |a| a.string.match(ANNOTATION_PREPEND_BEFORE) })
177
+ [:prepend_before, anno.string.match(ANNOTATION_PREPEND_BEFORE)&.[](1) || ""]
178
+ end
102
179
  end
103
180
  end
104
181
  end
data/sig/rbs/patch.rbs CHANGED
@@ -2,12 +2,40 @@ module RBS
2
2
  class Patch
3
3
  VERSION: String
4
4
 
5
- @env: untyped
5
+ @decls: ::Array[::RBS::AST::Declarations::t]
6
6
 
7
- def initialize: () -> void
7
+ ANNOTATION_OVERRIDE: "patch:override"
8
+
9
+ ANNOTATION_DELETE: "patch:delete"
10
+
11
+ ANNOTATION_APPEND_AFTER: ::Regexp
12
+
13
+ ANNOTATION_PREPEND_BEFORE: ::Regexp
8
14
 
9
- def apply: (String source?, path: Pathname?) -> void
15
+ def initialize: () -> void
10
16
 
11
17
  def to_s: () -> String
18
+
19
+ def apply: (?untyped? String, ?path: Pathname?) -> void
20
+
21
+ type t = ::RBS::AST::Declarations::t | ::RBS::AST::Members::t
22
+
23
+ private
24
+
25
+ def extract_name: (t decl) -> String
26
+
27
+ def extract_members: (t decl) -> (Array[::RBS::AST::Declarations::Class::member] | Array[::RBS::AST::Declarations::Module::member] | nil)
28
+
29
+ def walk: (Array[t] decls, ?Array[String] name_stack) { (::RBS::AST::Declarations::t | ::RBS::AST::Members::t, String) -> void } -> void
30
+
31
+ def decl_map: () -> Hash[String, t]
32
+
33
+ def add: (t decl, to: String, ?after: String?, ?before: String?) -> void
34
+
35
+ def override: (String name, with: t) -> void
36
+
37
+ def delete: (String name) -> void
38
+
39
+ def process_annotations: (Array[::RBS::AST::Annotation] annotations) -> ([:override, nil] | [:delte, nil] | [:append_after, String] | [:prepend_before, String] | nil)
12
40
  end
13
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbs-patch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Koji NAKAMURA
@@ -13,16 +13,16 @@ dependencies:
13
13
  name: rbs
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - '='
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 4.0.0.dev.5
18
+ version: '3.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - '='
23
+ - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 4.0.0.dev.5
25
+ version: '3.0'
26
26
  description: RBS::Patch manages RBS (Ruby Signature) type definitions through patches.
27
27
  It applies incremental changes to existing RBS signatures.
28
28
  email:
@@ -35,6 +35,7 @@ files:
35
35
  - LICENSE.txt
36
36
  - README.md
37
37
  - Rakefile
38
+ - Steepfile
38
39
  - exe/rbs-patch
39
40
  - lib/rbs/patch.rb
40
41
  - lib/rbs/patch/version.rb