Class: Bundler::GemBytes::Actions::Gemspec::UpsertDependency

Inherits:
Object
  • Object
show all
Defined in:
lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb

Overview

Add or update a dependency in a gemspec

If a dependency on the given gem is not found, a new dependency is added to the end of the Gem::Specification block.

If one or more dependencies are found on the same gem as new_dependency, the version constraint is updated to the new_dependency version constraint.

The gemspec is updated via calls to the tree_rewriter object.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tree_rewriter, gemspec_block, receiver_name, dependencies) ⇒ UpsertDependency

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Initializes the upsert dependency action

Parameters:

  • tree_rewriter (Parser::TreeRewriter)

    The object that updates the source

  • gemspec_block (Parser::AST::Node)

    The Gem::Specification block

  • receiver_name (Symbol)

    The name of the receiver for the Gem::Specification block

  • dependencies (Array<DependencyNode>)

    The dependency declarations found in the gemspec file



54
55
56
57
58
59
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 54

def initialize(tree_rewriter, gemspec_block, receiver_name, dependencies)
  @tree_rewriter = tree_rewriter
  @gemspec_block = gemspec_block
  @receiver_name = receiver_name
  @dependencies = dependencies
end

Instance Attribute Details

#dependenciesArray<DependencyNode> (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The dependency declarations found in the gemspec file

Returns:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 47

class UpsertDependency
  # Initializes the upsert dependency action
  # @param tree_rewriter [Parser::TreeRewriter] The object that updates the source
  # @param gemspec_block [Parser::AST::Node] The Gem::Specification block
  # @param receiver_name [Symbol] The name of the receiver for the Gem::Specification block
  # @param dependencies [Array<DependencyNode>] The dependency declarations found in the gemspec file
  # @api private
  def initialize(tree_rewriter, gemspec_block, receiver_name, dependencies)
    @tree_rewriter = tree_rewriter
    @gemspec_block = gemspec_block
    @receiver_name = receiver_name
    @dependencies = dependencies
  end

  attr_reader :tree_rewriter, :gemspec_block, :receiver_name, :dependencies, :new_dependency

  # Adds or updates a dependency to the Gem::Specification block
  #
  # @example
  #   upsert_dependency = UpsertDependency.new(tree_rewriter, gemspec_block, receiver_name, dependencies)
  #   new_dependency = Dependency.new(:add_runtime_dependency, 'rubocop', '~> 1.68')
  #   upsert_dependency.call(new_dependency)
  # @param new_dependency [Dependency] The dependency declaration to add or update
  # @return [void]
  # @api public
  def call(new_dependency)
    @new_dependency = new_dependency
    matching_dependencies = dependencies.select { |d| d.dependency.gem_name == new_dependency.gem_name }

    if matching_dependencies.any?
      update_dependencies(matching_dependencies)
    else
      add_dependency
    end
  end

  # Update the version constraint of the existing dependency(s)
  # @param matching_dependencies [Array<DependencyNode>] The existing dependencies that match new_dependency
  # @return [void]
  # @api private
  def update_dependencies(matching_dependencies)
    matching_dependencies.each do |found_dependency|
      raise(dependency_type_conflict_error(found_dependency)) unless dependency_type_match?(found_dependency)

      tree_rewriter.replace(found_dependency.node.loc.expression, dependency_source_code(found_dependency))
    end
  end

  # Add the new_dependency to the end of the Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency
    # Add the dependency to the end of the Gem::Specification block
    internal_block = gemspec_block.children[2]
    if internal_block
      tree_rewriter.insert_after(internal_block.children.last.loc.expression, "\n  #{dependency_source_code}")
    else
      # When the Gem::Specification block is empty, it require special handling
      add_dependency_to_empty_gemspec_block
    end
  end

  # Error message for a dependency type conflict
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The error message
  # @api private
  def dependency_type_conflict_error(existing_dependency)
    # :nocov: JRuby give false positive for this line being uncovered by tests
    <<~MESSAGE.chomp.gsub("\n", ' ')
      Trying to add a
      #{dependency_method_to_type(new_dependency.method_name).upcase}
      dependency on "#{new_dependency.gem_name}" which conflicts with the existing
      #{dependency_method_to_type(existing_dependency.dependency.method_name).upcase}
      dependency.
    MESSAGE
    # :nocov:
  end

  # The dependency type (:runtime or :development) based on a given method name
  # @param method [Symbol] The method name to convert to a dependency type
  # @return [Symbol] The dependency type
  # @api private
  def dependency_method_to_type(method)
    method == :add_development_dependency ? :development : :runtime
  end

  # Checks if the new dependency type is the same as the existing dependency type
  #
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [Boolean] Whether the dependency type conflicts
  # @api private
  def dependency_type_match?(existing_dependency)
    # Either both are :add_development_dependency or both are not
    (existing_dependency.dependency.method_name == :add_development_dependency) ==
      (new_dependency.method_name == :add_development_dependency)
  end

  # Add new_dependency to an empty Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency_to_empty_gemspec_block
    source = gemspec_block.loc.expression.source
    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    tree_rewriter.replace(gemspec_block.loc.expression, <<~GEMSPEC_BLOCK.chomp)
      #{source[0..-5]}
        #{dependency_source_code}
      #{source[-3..]}
    GEMSPEC_BLOCK
    # :nocov:
  end

  # The source code for the updated dependency declaration
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The source code for the dependency declaration
  # @api private
  def dependency_source_code(existing_dependency = nil)
    # Use existing quote character for string literals
    q = new_quote_char(existing_dependency)

    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    "#{receiver_name}.#{new_method_name(existing_dependency)} " \
      "#{q}#{new_dependency.gem_name}#{q}, " \
      "#{new_dependency.version_constraints.map { |vc| "#{q}#{vc}#{q}" }.join(', ')}"
    # :nocov:
  end

  # Use the same quote char as the existing dependency or default to single quote
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [String] The quote character to use
  # @api private
  def new_quote_char(existing_dependency)
    if existing_dependency
      existing_dependency.node.children[3].loc.expression.source[0]
    else
      "'"
    end
  end

  # The method to use for the new dependency
  #
  # If `existing_dependency` is given and the dependency type (runtime vs.
  # development) matches, the existing dependency method is used. Otherwise,
  # the new_dependency method is used.
  #
  # The purpose of this method is ensure that an #add_dependency call is not
  # replaced with an #add_runtime_dependency call or vice versa. This
  # maintains consistency within the user's gemspec even though these methods
  # are functionally equivalent.
  #
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [Symbol] The method to use for the new dependency
  # @api private
  def new_method_name(existing_dependency)
    if existing_dependency && dependency_type_match?(existing_dependency)
      existing_dependency.dependency.method_name
    else
      new_dependency.method_name
    end
  end
end

#gemspec_blockParser::AST::Node (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The root AST node of the Gem::Specification block from the gemspec

Returns:

  • (Parser::AST::Node)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 47

class UpsertDependency
  # Initializes the upsert dependency action
  # @param tree_rewriter [Parser::TreeRewriter] The object that updates the source
  # @param gemspec_block [Parser::AST::Node] The Gem::Specification block
  # @param receiver_name [Symbol] The name of the receiver for the Gem::Specification block
  # @param dependencies [Array<DependencyNode>] The dependency declarations found in the gemspec file
  # @api private
  def initialize(tree_rewriter, gemspec_block, receiver_name, dependencies)
    @tree_rewriter = tree_rewriter
    @gemspec_block = gemspec_block
    @receiver_name = receiver_name
    @dependencies = dependencies
  end

  attr_reader :tree_rewriter, :gemspec_block, :receiver_name, :dependencies, :new_dependency

  # Adds or updates a dependency to the Gem::Specification block
  #
  # @example
  #   upsert_dependency = UpsertDependency.new(tree_rewriter, gemspec_block, receiver_name, dependencies)
  #   new_dependency = Dependency.new(:add_runtime_dependency, 'rubocop', '~> 1.68')
  #   upsert_dependency.call(new_dependency)
  # @param new_dependency [Dependency] The dependency declaration to add or update
  # @return [void]
  # @api public
  def call(new_dependency)
    @new_dependency = new_dependency
    matching_dependencies = dependencies.select { |d| d.dependency.gem_name == new_dependency.gem_name }

    if matching_dependencies.any?
      update_dependencies(matching_dependencies)
    else
      add_dependency
    end
  end

  # Update the version constraint of the existing dependency(s)
  # @param matching_dependencies [Array<DependencyNode>] The existing dependencies that match new_dependency
  # @return [void]
  # @api private
  def update_dependencies(matching_dependencies)
    matching_dependencies.each do |found_dependency|
      raise(dependency_type_conflict_error(found_dependency)) unless dependency_type_match?(found_dependency)

      tree_rewriter.replace(found_dependency.node.loc.expression, dependency_source_code(found_dependency))
    end
  end

  # Add the new_dependency to the end of the Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency
    # Add the dependency to the end of the Gem::Specification block
    internal_block = gemspec_block.children[2]
    if internal_block
      tree_rewriter.insert_after(internal_block.children.last.loc.expression, "\n  #{dependency_source_code}")
    else
      # When the Gem::Specification block is empty, it require special handling
      add_dependency_to_empty_gemspec_block
    end
  end

  # Error message for a dependency type conflict
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The error message
  # @api private
  def dependency_type_conflict_error(existing_dependency)
    # :nocov: JRuby give false positive for this line being uncovered by tests
    <<~MESSAGE.chomp.gsub("\n", ' ')
      Trying to add a
      #{dependency_method_to_type(new_dependency.method_name).upcase}
      dependency on "#{new_dependency.gem_name}" which conflicts with the existing
      #{dependency_method_to_type(existing_dependency.dependency.method_name).upcase}
      dependency.
    MESSAGE
    # :nocov:
  end

  # The dependency type (:runtime or :development) based on a given method name
  # @param method [Symbol] The method name to convert to a dependency type
  # @return [Symbol] The dependency type
  # @api private
  def dependency_method_to_type(method)
    method == :add_development_dependency ? :development : :runtime
  end

  # Checks if the new dependency type is the same as the existing dependency type
  #
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [Boolean] Whether the dependency type conflicts
  # @api private
  def dependency_type_match?(existing_dependency)
    # Either both are :add_development_dependency or both are not
    (existing_dependency.dependency.method_name == :add_development_dependency) ==
      (new_dependency.method_name == :add_development_dependency)
  end

  # Add new_dependency to an empty Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency_to_empty_gemspec_block
    source = gemspec_block.loc.expression.source
    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    tree_rewriter.replace(gemspec_block.loc.expression, <<~GEMSPEC_BLOCK.chomp)
      #{source[0..-5]}
        #{dependency_source_code}
      #{source[-3..]}
    GEMSPEC_BLOCK
    # :nocov:
  end

  # The source code for the updated dependency declaration
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The source code for the dependency declaration
  # @api private
  def dependency_source_code(existing_dependency = nil)
    # Use existing quote character for string literals
    q = new_quote_char(existing_dependency)

    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    "#{receiver_name}.#{new_method_name(existing_dependency)} " \
      "#{q}#{new_dependency.gem_name}#{q}, " \
      "#{new_dependency.version_constraints.map { |vc| "#{q}#{vc}#{q}" }.join(', ')}"
    # :nocov:
  end

  # Use the same quote char as the existing dependency or default to single quote
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [String] The quote character to use
  # @api private
  def new_quote_char(existing_dependency)
    if existing_dependency
      existing_dependency.node.children[3].loc.expression.source[0]
    else
      "'"
    end
  end

  # The method to use for the new dependency
  #
  # If `existing_dependency` is given and the dependency type (runtime vs.
  # development) matches, the existing dependency method is used. Otherwise,
  # the new_dependency method is used.
  #
  # The purpose of this method is ensure that an #add_dependency call is not
  # replaced with an #add_runtime_dependency call or vice versa. This
  # maintains consistency within the user's gemspec even though these methods
  # are functionally equivalent.
  #
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [Symbol] The method to use for the new dependency
  # @api private
  def new_method_name(existing_dependency)
    if existing_dependency && dependency_type_match?(existing_dependency)
      existing_dependency.dependency.method_name
    else
      new_dependency.method_name
    end
  end
end

#new_dependencyDependency (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The dependency declaration to add or update

Returns:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 47

class UpsertDependency
  # Initializes the upsert dependency action
  # @param tree_rewriter [Parser::TreeRewriter] The object that updates the source
  # @param gemspec_block [Parser::AST::Node] The Gem::Specification block
  # @param receiver_name [Symbol] The name of the receiver for the Gem::Specification block
  # @param dependencies [Array<DependencyNode>] The dependency declarations found in the gemspec file
  # @api private
  def initialize(tree_rewriter, gemspec_block, receiver_name, dependencies)
    @tree_rewriter = tree_rewriter
    @gemspec_block = gemspec_block
    @receiver_name = receiver_name
    @dependencies = dependencies
  end

  attr_reader :tree_rewriter, :gemspec_block, :receiver_name, :dependencies, :new_dependency

  # Adds or updates a dependency to the Gem::Specification block
  #
  # @example
  #   upsert_dependency = UpsertDependency.new(tree_rewriter, gemspec_block, receiver_name, dependencies)
  #   new_dependency = Dependency.new(:add_runtime_dependency, 'rubocop', '~> 1.68')
  #   upsert_dependency.call(new_dependency)
  # @param new_dependency [Dependency] The dependency declaration to add or update
  # @return [void]
  # @api public
  def call(new_dependency)
    @new_dependency = new_dependency
    matching_dependencies = dependencies.select { |d| d.dependency.gem_name == new_dependency.gem_name }

    if matching_dependencies.any?
      update_dependencies(matching_dependencies)
    else
      add_dependency
    end
  end

  # Update the version constraint of the existing dependency(s)
  # @param matching_dependencies [Array<DependencyNode>] The existing dependencies that match new_dependency
  # @return [void]
  # @api private
  def update_dependencies(matching_dependencies)
    matching_dependencies.each do |found_dependency|
      raise(dependency_type_conflict_error(found_dependency)) unless dependency_type_match?(found_dependency)

      tree_rewriter.replace(found_dependency.node.loc.expression, dependency_source_code(found_dependency))
    end
  end

  # Add the new_dependency to the end of the Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency
    # Add the dependency to the end of the Gem::Specification block
    internal_block = gemspec_block.children[2]
    if internal_block
      tree_rewriter.insert_after(internal_block.children.last.loc.expression, "\n  #{dependency_source_code}")
    else
      # When the Gem::Specification block is empty, it require special handling
      add_dependency_to_empty_gemspec_block
    end
  end

  # Error message for a dependency type conflict
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The error message
  # @api private
  def dependency_type_conflict_error(existing_dependency)
    # :nocov: JRuby give false positive for this line being uncovered by tests
    <<~MESSAGE.chomp.gsub("\n", ' ')
      Trying to add a
      #{dependency_method_to_type(new_dependency.method_name).upcase}
      dependency on "#{new_dependency.gem_name}" which conflicts with the existing
      #{dependency_method_to_type(existing_dependency.dependency.method_name).upcase}
      dependency.
    MESSAGE
    # :nocov:
  end

  # The dependency type (:runtime or :development) based on a given method name
  # @param method [Symbol] The method name to convert to a dependency type
  # @return [Symbol] The dependency type
  # @api private
  def dependency_method_to_type(method)
    method == :add_development_dependency ? :development : :runtime
  end

  # Checks if the new dependency type is the same as the existing dependency type
  #
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [Boolean] Whether the dependency type conflicts
  # @api private
  def dependency_type_match?(existing_dependency)
    # Either both are :add_development_dependency or both are not
    (existing_dependency.dependency.method_name == :add_development_dependency) ==
      (new_dependency.method_name == :add_development_dependency)
  end

  # Add new_dependency to an empty Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency_to_empty_gemspec_block
    source = gemspec_block.loc.expression.source
    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    tree_rewriter.replace(gemspec_block.loc.expression, <<~GEMSPEC_BLOCK.chomp)
      #{source[0..-5]}
        #{dependency_source_code}
      #{source[-3..]}
    GEMSPEC_BLOCK
    # :nocov:
  end

  # The source code for the updated dependency declaration
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The source code for the dependency declaration
  # @api private
  def dependency_source_code(existing_dependency = nil)
    # Use existing quote character for string literals
    q = new_quote_char(existing_dependency)

    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    "#{receiver_name}.#{new_method_name(existing_dependency)} " \
      "#{q}#{new_dependency.gem_name}#{q}, " \
      "#{new_dependency.version_constraints.map { |vc| "#{q}#{vc}#{q}" }.join(', ')}"
    # :nocov:
  end

  # Use the same quote char as the existing dependency or default to single quote
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [String] The quote character to use
  # @api private
  def new_quote_char(existing_dependency)
    if existing_dependency
      existing_dependency.node.children[3].loc.expression.source[0]
    else
      "'"
    end
  end

  # The method to use for the new dependency
  #
  # If `existing_dependency` is given and the dependency type (runtime vs.
  # development) matches, the existing dependency method is used. Otherwise,
  # the new_dependency method is used.
  #
  # The purpose of this method is ensure that an #add_dependency call is not
  # replaced with an #add_runtime_dependency call or vice versa. This
  # maintains consistency within the user's gemspec even though these methods
  # are functionally equivalent.
  #
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [Symbol] The method to use for the new dependency
  # @api private
  def new_method_name(existing_dependency)
    if existing_dependency && dependency_type_match?(existing_dependency)
      existing_dependency.dependency.method_name
    else
      new_dependency.method_name
    end
  end
end

#receiver_nameSymbol (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The name of the receiver for the Gem::Specification block

Returns:

  • (Symbol)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 47

class UpsertDependency
  # Initializes the upsert dependency action
  # @param tree_rewriter [Parser::TreeRewriter] The object that updates the source
  # @param gemspec_block [Parser::AST::Node] The Gem::Specification block
  # @param receiver_name [Symbol] The name of the receiver for the Gem::Specification block
  # @param dependencies [Array<DependencyNode>] The dependency declarations found in the gemspec file
  # @api private
  def initialize(tree_rewriter, gemspec_block, receiver_name, dependencies)
    @tree_rewriter = tree_rewriter
    @gemspec_block = gemspec_block
    @receiver_name = receiver_name
    @dependencies = dependencies
  end

  attr_reader :tree_rewriter, :gemspec_block, :receiver_name, :dependencies, :new_dependency

  # Adds or updates a dependency to the Gem::Specification block
  #
  # @example
  #   upsert_dependency = UpsertDependency.new(tree_rewriter, gemspec_block, receiver_name, dependencies)
  #   new_dependency = Dependency.new(:add_runtime_dependency, 'rubocop', '~> 1.68')
  #   upsert_dependency.call(new_dependency)
  # @param new_dependency [Dependency] The dependency declaration to add or update
  # @return [void]
  # @api public
  def call(new_dependency)
    @new_dependency = new_dependency
    matching_dependencies = dependencies.select { |d| d.dependency.gem_name == new_dependency.gem_name }

    if matching_dependencies.any?
      update_dependencies(matching_dependencies)
    else
      add_dependency
    end
  end

  # Update the version constraint of the existing dependency(s)
  # @param matching_dependencies [Array<DependencyNode>] The existing dependencies that match new_dependency
  # @return [void]
  # @api private
  def update_dependencies(matching_dependencies)
    matching_dependencies.each do |found_dependency|
      raise(dependency_type_conflict_error(found_dependency)) unless dependency_type_match?(found_dependency)

      tree_rewriter.replace(found_dependency.node.loc.expression, dependency_source_code(found_dependency))
    end
  end

  # Add the new_dependency to the end of the Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency
    # Add the dependency to the end of the Gem::Specification block
    internal_block = gemspec_block.children[2]
    if internal_block
      tree_rewriter.insert_after(internal_block.children.last.loc.expression, "\n  #{dependency_source_code}")
    else
      # When the Gem::Specification block is empty, it require special handling
      add_dependency_to_empty_gemspec_block
    end
  end

  # Error message for a dependency type conflict
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The error message
  # @api private
  def dependency_type_conflict_error(existing_dependency)
    # :nocov: JRuby give false positive for this line being uncovered by tests
    <<~MESSAGE.chomp.gsub("\n", ' ')
      Trying to add a
      #{dependency_method_to_type(new_dependency.method_name).upcase}
      dependency on "#{new_dependency.gem_name}" which conflicts with the existing
      #{dependency_method_to_type(existing_dependency.dependency.method_name).upcase}
      dependency.
    MESSAGE
    # :nocov:
  end

  # The dependency type (:runtime or :development) based on a given method name
  # @param method [Symbol] The method name to convert to a dependency type
  # @return [Symbol] The dependency type
  # @api private
  def dependency_method_to_type(method)
    method == :add_development_dependency ? :development : :runtime
  end

  # Checks if the new dependency type is the same as the existing dependency type
  #
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [Boolean] Whether the dependency type conflicts
  # @api private
  def dependency_type_match?(existing_dependency)
    # Either both are :add_development_dependency or both are not
    (existing_dependency.dependency.method_name == :add_development_dependency) ==
      (new_dependency.method_name == :add_development_dependency)
  end

  # Add new_dependency to an empty Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency_to_empty_gemspec_block
    source = gemspec_block.loc.expression.source
    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    tree_rewriter.replace(gemspec_block.loc.expression, <<~GEMSPEC_BLOCK.chomp)
      #{source[0..-5]}
        #{dependency_source_code}
      #{source[-3..]}
    GEMSPEC_BLOCK
    # :nocov:
  end

  # The source code for the updated dependency declaration
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The source code for the dependency declaration
  # @api private
  def dependency_source_code(existing_dependency = nil)
    # Use existing quote character for string literals
    q = new_quote_char(existing_dependency)

    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    "#{receiver_name}.#{new_method_name(existing_dependency)} " \
      "#{q}#{new_dependency.gem_name}#{q}, " \
      "#{new_dependency.version_constraints.map { |vc| "#{q}#{vc}#{q}" }.join(', ')}"
    # :nocov:
  end

  # Use the same quote char as the existing dependency or default to single quote
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [String] The quote character to use
  # @api private
  def new_quote_char(existing_dependency)
    if existing_dependency
      existing_dependency.node.children[3].loc.expression.source[0]
    else
      "'"
    end
  end

  # The method to use for the new dependency
  #
  # If `existing_dependency` is given and the dependency type (runtime vs.
  # development) matches, the existing dependency method is used. Otherwise,
  # the new_dependency method is used.
  #
  # The purpose of this method is ensure that an #add_dependency call is not
  # replaced with an #add_runtime_dependency call or vice versa. This
  # maintains consistency within the user's gemspec even though these methods
  # are functionally equivalent.
  #
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [Symbol] The method to use for the new dependency
  # @api private
  def new_method_name(existing_dependency)
    if existing_dependency && dependency_type_match?(existing_dependency)
      existing_dependency.dependency.method_name
    else
      new_dependency.method_name
    end
  end
end

#tree_rewriterParser::TreeRewriter (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The object that updates the source

Returns:

  • (Parser::TreeRewriter)


47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 47

class UpsertDependency
  # Initializes the upsert dependency action
  # @param tree_rewriter [Parser::TreeRewriter] The object that updates the source
  # @param gemspec_block [Parser::AST::Node] The Gem::Specification block
  # @param receiver_name [Symbol] The name of the receiver for the Gem::Specification block
  # @param dependencies [Array<DependencyNode>] The dependency declarations found in the gemspec file
  # @api private
  def initialize(tree_rewriter, gemspec_block, receiver_name, dependencies)
    @tree_rewriter = tree_rewriter
    @gemspec_block = gemspec_block
    @receiver_name = receiver_name
    @dependencies = dependencies
  end

  attr_reader :tree_rewriter, :gemspec_block, :receiver_name, :dependencies, :new_dependency

  # Adds or updates a dependency to the Gem::Specification block
  #
  # @example
  #   upsert_dependency = UpsertDependency.new(tree_rewriter, gemspec_block, receiver_name, dependencies)
  #   new_dependency = Dependency.new(:add_runtime_dependency, 'rubocop', '~> 1.68')
  #   upsert_dependency.call(new_dependency)
  # @param new_dependency [Dependency] The dependency declaration to add or update
  # @return [void]
  # @api public
  def call(new_dependency)
    @new_dependency = new_dependency
    matching_dependencies = dependencies.select { |d| d.dependency.gem_name == new_dependency.gem_name }

    if matching_dependencies.any?
      update_dependencies(matching_dependencies)
    else
      add_dependency
    end
  end

  # Update the version constraint of the existing dependency(s)
  # @param matching_dependencies [Array<DependencyNode>] The existing dependencies that match new_dependency
  # @return [void]
  # @api private
  def update_dependencies(matching_dependencies)
    matching_dependencies.each do |found_dependency|
      raise(dependency_type_conflict_error(found_dependency)) unless dependency_type_match?(found_dependency)

      tree_rewriter.replace(found_dependency.node.loc.expression, dependency_source_code(found_dependency))
    end
  end

  # Add the new_dependency to the end of the Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency
    # Add the dependency to the end of the Gem::Specification block
    internal_block = gemspec_block.children[2]
    if internal_block
      tree_rewriter.insert_after(internal_block.children.last.loc.expression, "\n  #{dependency_source_code}")
    else
      # When the Gem::Specification block is empty, it require special handling
      add_dependency_to_empty_gemspec_block
    end
  end

  # Error message for a dependency type conflict
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The error message
  # @api private
  def dependency_type_conflict_error(existing_dependency)
    # :nocov: JRuby give false positive for this line being uncovered by tests
    <<~MESSAGE.chomp.gsub("\n", ' ')
      Trying to add a
      #{dependency_method_to_type(new_dependency.method_name).upcase}
      dependency on "#{new_dependency.gem_name}" which conflicts with the existing
      #{dependency_method_to_type(existing_dependency.dependency.method_name).upcase}
      dependency.
    MESSAGE
    # :nocov:
  end

  # The dependency type (:runtime or :development) based on a given method name
  # @param method [Symbol] The method name to convert to a dependency type
  # @return [Symbol] The dependency type
  # @api private
  def dependency_method_to_type(method)
    method == :add_development_dependency ? :development : :runtime
  end

  # Checks if the new dependency type is the same as the existing dependency type
  #
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [Boolean] Whether the dependency type conflicts
  # @api private
  def dependency_type_match?(existing_dependency)
    # Either both are :add_development_dependency or both are not
    (existing_dependency.dependency.method_name == :add_development_dependency) ==
      (new_dependency.method_name == :add_development_dependency)
  end

  # Add new_dependency to an empty Gem::Specification block
  # @return [void]
  # @api private
  def add_dependency_to_empty_gemspec_block
    source = gemspec_block.loc.expression.source
    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    tree_rewriter.replace(gemspec_block.loc.expression, <<~GEMSPEC_BLOCK.chomp)
      #{source[0..-5]}
        #{dependency_source_code}
      #{source[-3..]}
    GEMSPEC_BLOCK
    # :nocov:
  end

  # The source code for the updated dependency declaration
  # @param existing_dependency [DependencyNode] The existing dependency
  # @return [String] The source code for the dependency declaration
  # @api private
  def dependency_source_code(existing_dependency = nil)
    # Use existing quote character for string literals
    q = new_quote_char(existing_dependency)

    # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
    "#{receiver_name}.#{new_method_name(existing_dependency)} " \
      "#{q}#{new_dependency.gem_name}#{q}, " \
      "#{new_dependency.version_constraints.map { |vc| "#{q}#{vc}#{q}" }.join(', ')}"
    # :nocov:
  end

  # Use the same quote char as the existing dependency or default to single quote
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [String] The quote character to use
  # @api private
  def new_quote_char(existing_dependency)
    if existing_dependency
      existing_dependency.node.children[3].loc.expression.source[0]
    else
      "'"
    end
  end

  # The method to use for the new dependency
  #
  # If `existing_dependency` is given and the dependency type (runtime vs.
  # development) matches, the existing dependency method is used. Otherwise,
  # the new_dependency method is used.
  #
  # The purpose of this method is ensure that an #add_dependency call is not
  # replaced with an #add_runtime_dependency call or vice versa. This
  # maintains consistency within the user's gemspec even though these methods
  # are functionally equivalent.
  #
  # @param existing_dependency [DependencyNode, nil] The existing dependency being updated
  # @return [Symbol] The method to use for the new dependency
  # @api private
  def new_method_name(existing_dependency)
    if existing_dependency && dependency_type_match?(existing_dependency)
      existing_dependency.dependency.method_name
    else
      new_dependency.method_name
    end
  end
end

Instance Method Details

#add_dependencyvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Add the new_dependency to the end of the Gem::Specification block



98
99
100
101
102
103
104
105
106
107
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 98

def add_dependency
  # Add the dependency to the end of the Gem::Specification block
  internal_block = gemspec_block.children[2]
  if internal_block
    tree_rewriter.insert_after(internal_block.children.last.loc.expression, "\n  #{dependency_source_code}")
  else
    # When the Gem::Specification block is empty, it require special handling
    add_dependency_to_empty_gemspec_block
  end
end

#add_dependency_to_empty_gemspec_blockvoid

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Add new_dependency to an empty Gem::Specification block



147
148
149
150
151
152
153
154
155
156
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 147

def add_dependency_to_empty_gemspec_block
  source = gemspec_block.loc.expression.source
  # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
  tree_rewriter.replace(gemspec_block.loc.expression, <<~GEMSPEC_BLOCK.chomp)
    #{source[0..-5]}
      #{dependency_source_code}
    #{source[-3..]}
  GEMSPEC_BLOCK
  # :nocov:
end

#call(new_dependency) ⇒ void

This method returns an undefined value.

Adds or updates a dependency to the Gem::Specification block

Examples:

upsert_dependency = UpsertDependency.new(tree_rewriter, gemspec_block, receiver_name, dependencies)
new_dependency = Dependency.new(:add_runtime_dependency, 'rubocop', '~> 1.68')
upsert_dependency.call(new_dependency)

Parameters:

  • new_dependency (Dependency)

    The dependency declaration to add or update



72
73
74
75
76
77
78
79
80
81
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 72

def call(new_dependency)
  @new_dependency = new_dependency
  matching_dependencies = dependencies.select { |d| d.dependency.gem_name == new_dependency.gem_name }

  if matching_dependencies.any?
    update_dependencies(matching_dependencies)
  else
    add_dependency
  end
end

#dependency_method_to_type(method) ⇒ Symbol

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The dependency type (:runtime or :development) based on a given method name

Parameters:

  • method (Symbol)

    The method name to convert to a dependency type

Returns:

  • (Symbol)

    The dependency type



129
130
131
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 129

def dependency_method_to_type(method)
  method == :add_development_dependency ? :development : :runtime
end

#dependency_source_code(existing_dependency = nil) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The source code for the updated dependency declaration

Parameters:

  • existing_dependency (DependencyNode) (defaults to: nil)

    The existing dependency

Returns:

  • (String)

    The source code for the dependency declaration



162
163
164
165
166
167
168
169
170
171
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 162

def dependency_source_code(existing_dependency = nil)
  # Use existing quote character for string literals
  q = new_quote_char(existing_dependency)

  # :nocov: supress false reporting of no coverage of multiline string literals on JRuby
  "#{receiver_name}.#{new_method_name(existing_dependency)} " \
    "#{q}#{new_dependency.gem_name}#{q}, " \
    "#{new_dependency.version_constraints.map { |vc| "#{q}#{vc}#{q}" }.join(', ')}"
  # :nocov:
end

#dependency_type_conflict_error(existing_dependency) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Error message for a dependency type conflict

Parameters:

Returns:

  • (String)

    The error message



113
114
115
116
117
118
119
120
121
122
123
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 113

def dependency_type_conflict_error(existing_dependency)
  # :nocov: JRuby give false positive for this line being uncovered by tests
  <<~MESSAGE.chomp.gsub("\n", ' ')
    Trying to add a
    #{dependency_method_to_type(new_dependency.method_name).upcase}
    dependency on "#{new_dependency.gem_name}" which conflicts with the existing
    #{dependency_method_to_type(existing_dependency.dependency.method_name).upcase}
    dependency.
  MESSAGE
  # :nocov:
end

#dependency_type_match?(existing_dependency) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Checks if the new dependency type is the same as the existing dependency type

Parameters:

Returns:

  • (Boolean)

    Whether the dependency type conflicts



138
139
140
141
142
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 138

def dependency_type_match?(existing_dependency)
  # Either both are :add_development_dependency or both are not
  (existing_dependency.dependency.method_name == :add_development_dependency) ==
    (new_dependency.method_name == :add_development_dependency)
end

#new_method_name(existing_dependency) ⇒ Symbol

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The method to use for the new dependency

If ‘existing_dependency` is given and the dependency type (runtime vs. development) matches, the existing dependency method is used. Otherwise, the new_dependency method is used.

The purpose of this method is ensure that an #add_dependency call is not replaced with an #add_runtime_dependency call or vice versa. This maintains consistency within the user’s gemspec even though these methods are functionally equivalent.

Parameters:

  • existing_dependency (DependencyNode, nil)

    The existing dependency being updated

Returns:

  • (Symbol)

    The method to use for the new dependency



199
200
201
202
203
204
205
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 199

def new_method_name(existing_dependency)
  if existing_dependency && dependency_type_match?(existing_dependency)
    existing_dependency.dependency.method_name
  else
    new_dependency.method_name
  end
end

#new_quote_char(existing_dependency) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Use the same quote char as the existing dependency or default to single quote

Parameters:

  • existing_dependency (DependencyNode, nil)

    The existing dependency being updated

Returns:

  • (String)

    The quote character to use



177
178
179
180
181
182
183
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 177

def new_quote_char(existing_dependency)
  if existing_dependency
    existing_dependency.node.children[3].loc.expression.source[0]
  else
    "'"
  end
end

#update_dependencies(matching_dependencies) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Update the version constraint of the existing dependency(s)

Parameters:

  • matching_dependencies (Array<DependencyNode>)

    The existing dependencies that match new_dependency



87
88
89
90
91
92
93
# File 'lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb', line 87

def update_dependencies(matching_dependencies)
  matching_dependencies.each do |found_dependency|
    raise(dependency_type_conflict_error(found_dependency)) unless dependency_type_match?(found_dependency)

    tree_rewriter.replace(found_dependency.node.loc.expression, dependency_source_code(found_dependency))
  end
end