dependabot-bazel 0.344.0

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.
@@ -0,0 +1,203 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/bazel/file_updater"
5
+
6
+ module Dependabot
7
+ module Bazel
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ class WorkspaceFileUpdater
10
+ extend T::Sig
11
+
12
+ sig do
13
+ params(
14
+ dependency_files: T::Array[Dependabot::DependencyFile],
15
+ dependencies: T::Array[Dependabot::Dependency],
16
+ credentials: T::Array[Dependabot::Credential]
17
+ ).void
18
+ end
19
+ def initialize(dependency_files:, dependencies:, credentials:)
20
+ @dependency_files = dependency_files
21
+ @dependencies = dependencies
22
+ @credentials = credentials
23
+ end
24
+
25
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
26
+ def updated_workspace_files
27
+ workspace_files.filter_map do |file|
28
+ updated_content = update_file_content(file)
29
+ next if updated_content == T.must(file.content)
30
+
31
+ file.dup.tap { |f| f.content = updated_content }
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
38
+ attr_reader :dependency_files
39
+
40
+ sig { returns(T::Array[Dependabot::Dependency]) }
41
+ attr_reader :dependencies
42
+
43
+ sig { returns(T::Array[Dependabot::Credential]) }
44
+ attr_reader :credentials
45
+
46
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
47
+ def workspace_files
48
+ @workspace_files ||= T.let(
49
+ dependency_files.select do |f|
50
+ f.name == "WORKSPACE" || f.name.end_with?("WORKSPACE.bazel")
51
+ end,
52
+ T.nilable(T::Array[Dependabot::DependencyFile])
53
+ )
54
+ end
55
+
56
+ sig { params(file: Dependabot::DependencyFile).returns(String) }
57
+ def update_file_content(file)
58
+ content = T.must(file.content).dup
59
+
60
+ dependencies.each do |dependency|
61
+ content = update_dependency_in_content(content, dependency)
62
+ end
63
+
64
+ content
65
+ end
66
+
67
+ sig { params(content: String, dependency: Dependabot::Dependency).returns(String) }
68
+ def update_dependency_in_content(content, dependency)
69
+ return content unless dependency.package_manager == "bazel"
70
+
71
+ case dependency_type(dependency)
72
+ when :http_archive
73
+ update_http_archive_declaration(content, dependency)
74
+ when :git_repository
75
+ update_git_repository_declaration(content, dependency)
76
+ else
77
+ content
78
+ end
79
+ end
80
+
81
+ sig { params(dependency: Dependabot::Dependency).returns(Symbol) }
82
+ def dependency_type(dependency)
83
+ return :http_archive if dependency.requirements.any? { |req| req.dig(:source, :type) == "http_archive" }
84
+ return :git_repository if dependency.requirements.any? { |req| req.dig(:source, :type) == "git_repository" }
85
+
86
+ :unknown
87
+ end
88
+
89
+ sig { params(content: String, dependency: Dependabot::Dependency).returns(String) }
90
+ def update_http_archive_declaration(content, dependency)
91
+ new_version = dependency.version
92
+ return content unless new_version
93
+
94
+ escaped_name = Regexp.escape(dependency.name)
95
+
96
+ http_archive_pattern = /http_archive\s*\(([^)]+?)\)/mx
97
+
98
+ content.gsub(http_archive_pattern) do |match|
99
+ function_content = T.must(Regexp.last_match(1))
100
+
101
+ if /name\s*=\s*["']#{escaped_name}["']/.match?(function_content)
102
+ updated_function_content = update_http_archive_attributes(function_content, dependency)
103
+ "http_archive(#{updated_function_content})"
104
+ else
105
+ match
106
+ end
107
+ end
108
+ rescue Dependabot::DependencyFileNotResolvable => e
109
+ raise e
110
+ rescue StandardError => e
111
+ raise Dependabot::DependencyFileNotResolvable,
112
+ "Failed to update http_archive for #{dependency.name}: #{e.message}"
113
+ end
114
+
115
+ sig { params(function_content: String, dependency: Dependabot::Dependency).returns(String) }
116
+ def update_http_archive_attributes(function_content, dependency)
117
+ updated_content = function_content.dup
118
+
119
+ updated_content = update_archive_url(updated_content, dependency) if /url\s*=/.match?(updated_content)
120
+
121
+ updated_content = update_archive_urls_array(updated_content, dependency) if /urls\s*=/.match?(updated_content)
122
+
123
+ updated_content
124
+ end
125
+
126
+ sig { params(content: String, dependency: Dependabot::Dependency).returns(String) }
127
+ def update_archive_url(content, dependency)
128
+ old_version = dependency.previous_version
129
+ new_version = dependency.version
130
+ return content unless old_version && new_version
131
+
132
+ content.gsub(/url\s*=\s*["']([^"']+)["']/) do
133
+ url = T.must(Regexp.last_match(1))
134
+ updated_url = transform_version_in_url(url, old_version, new_version)
135
+ "url = \"#{updated_url}\""
136
+ end
137
+ end
138
+
139
+ sig { params(content: String, dependency: Dependabot::Dependency).returns(String) }
140
+ def update_archive_urls_array(content, dependency)
141
+ old_version = dependency.previous_version
142
+ new_version = dependency.version
143
+ return content unless old_version && new_version
144
+
145
+ content.gsub(/urls\s*=\s*\[(.*?)\]/m) do
146
+ urls_content = T.must(Regexp.last_match(1))
147
+ updated_urls_content = urls_content.gsub(/["']([^"']+)["']/) do
148
+ url = T.must(Regexp.last_match(1))
149
+ updated_url = transform_version_in_url(url, old_version, new_version)
150
+ "\"#{updated_url}\""
151
+ end
152
+ "urls = [#{updated_urls_content}]"
153
+ end
154
+ end
155
+
156
+ sig { params(url: String, old_version: String, new_version: String).returns(String) }
157
+ def transform_version_in_url(url, old_version, new_version)
158
+ return url.gsub(old_version, new_version) if url.include?(old_version)
159
+
160
+ return url.gsub("v#{old_version}", "v#{new_version}") if url.include?("v#{old_version}")
161
+
162
+ old_with_v = "v#{old_version}"
163
+ return url.gsub(old_with_v, "v#{new_version}") if url.include?(old_with_v)
164
+
165
+ url
166
+ end
167
+
168
+ sig { params(content: String, dependency: Dependabot::Dependency).returns(String) }
169
+ def update_git_repository_declaration(content, dependency)
170
+ new_version = dependency.version
171
+ return content unless new_version
172
+
173
+ escaped_name = Regexp.escape(dependency.name)
174
+
175
+ git_repo_pattern = /git_repository\s*\(([^)]+?)\)/mx
176
+
177
+ content.gsub(git_repo_pattern) do |match|
178
+ function_content = T.must(Regexp.last_match(1))
179
+
180
+ if /name\s*=\s*["']#{escaped_name}["']/.match?(function_content)
181
+ updated_function_content = if /tag\s*=/.match?(function_content)
182
+ function_content.gsub(
183
+ /tag\s*=\s*["'][^"']*["']/,
184
+ "tag = \"#{new_version}\""
185
+ )
186
+ elsif /commit\s*=/.match?(function_content)
187
+ function_content.gsub(
188
+ /commit\s*=\s*["'][^"']*["']/,
189
+ "commit = \"#{new_version}\""
190
+ )
191
+ else
192
+ function_content
193
+ end
194
+ "git_repository(#{updated_function_content})"
195
+ else
196
+ match
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,116 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/file_updaters"
5
+ require "dependabot/file_updaters/base"
6
+
7
+ module Dependabot
8
+ module Bazel
9
+ class FileUpdater < Dependabot::FileUpdaters::Base
10
+ extend T::Sig
11
+
12
+ require_relative "file_updater/bzlmod_file_updater"
13
+ require_relative "file_updater/workspace_file_updater"
14
+ require_relative "file_updater/declaration_parser"
15
+
16
+ sig { returns(T::Array[Regexp]) }
17
+ def self.updated_files_regex
18
+ [
19
+ /^MODULE\.bazel$/,
20
+ %r{^(?:.*/)?MODULE\.bazel$},
21
+ /^WORKSPACE$/,
22
+ %r{^(?:.*/)?WORKSPACE\.bazel$},
23
+ %r{^(?:.*/)?BUILD$},
24
+ %r{^(?:.*/)?BUILD\.bazel$}
25
+ ]
26
+ end
27
+
28
+ sig { override.returns(T::Array[Dependabot::DependencyFile]) }
29
+ def updated_dependency_files
30
+ updated_files = T.let([], T::Array[Dependabot::DependencyFile])
31
+
32
+ dependencies.each do |dependency|
33
+ if bzlmod_dependency?(dependency)
34
+ updated_files.concat(update_bzlmod_dependency(dependency))
35
+ elsif workspace_dependency?(dependency)
36
+ updated_files.concat(update_workspace_dependency(dependency))
37
+ end
38
+ end
39
+
40
+ updated_files.uniq
41
+ end
42
+
43
+ private
44
+
45
+ sig { override.void }
46
+ def check_required_files
47
+ return if module_files.any? || workspace_files.any?
48
+
49
+ raise Dependabot::DependencyFileNotFound.new(
50
+ nil,
51
+ "No MODULE.bazel or WORKSPACE file found!"
52
+ )
53
+ end
54
+
55
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
56
+ def module_files
57
+ @module_files ||= T.let(
58
+ dependency_files.select { |f| f.name.end_with?("MODULE.bazel") },
59
+ T.nilable(T::Array[Dependabot::DependencyFile])
60
+ )
61
+ end
62
+
63
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
64
+ def workspace_files
65
+ @workspace_files ||= T.let(
66
+ dependency_files.select do |f|
67
+ f.name == "WORKSPACE" || f.name.end_with?("WORKSPACE.bazel")
68
+ end,
69
+ T.nilable(T::Array[Dependabot::DependencyFile])
70
+ )
71
+ end
72
+
73
+ sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) }
74
+ def bzlmod_dependency?(dependency)
75
+ dependency.requirements.any? { |req| req[:file]&.end_with?("MODULE.bazel") }
76
+ end
77
+
78
+ sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) }
79
+ def workspace_dependency?(dependency)
80
+ dependency.requirements.any? do |req|
81
+ req[:file] == "WORKSPACE" || req[:file]&.end_with?("WORKSPACE.bazel")
82
+ end
83
+ end
84
+
85
+ sig { params(dependency: Dependabot::Dependency).returns(T::Array[Dependabot::DependencyFile]) }
86
+ def update_bzlmod_dependency(dependency)
87
+ bzlmod_updater = BzlmodFileUpdater.new(
88
+ dependency_files: dependency_files,
89
+ dependencies: [dependency],
90
+ credentials: credentials
91
+ )
92
+ bzlmod_updater.updated_module_files
93
+ end
94
+
95
+ sig { params(dependency: Dependabot::Dependency).returns(T::Array[Dependabot::DependencyFile]) }
96
+ def update_workspace_dependency(dependency)
97
+ workspace_updater = WorkspaceFileUpdater.new(
98
+ dependency_files: dependency_files,
99
+ dependencies: [dependency],
100
+ credentials: credentials
101
+ )
102
+ workspace_updater.updated_workspace_files
103
+ end
104
+
105
+ sig { params(file: Dependabot::DependencyFile).returns(T::Array[Dependabot::Dependency]) }
106
+ def relevant_dependencies_for_file(file)
107
+ dependencies.select do |dependency|
108
+ dependency.package_manager == "bazel" &&
109
+ dependency.requirements.any? { |req| req[:file] == file.name }
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ Dependabot::FileUpdaters.register("bazel", Dependabot::Bazel::FileUpdater)
@@ -0,0 +1,26 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/bazel/version"
6
+ require "dependabot/bazel/requirement"
7
+ require "dependabot/ecosystem"
8
+
9
+ module Dependabot
10
+ module Bazel
11
+ LANGUAGE = "bazel"
12
+
13
+ class Language < Dependabot::Ecosystem::VersionManager
14
+ extend T::Sig
15
+
16
+ sig { params(raw_version: String, requirement: T.nilable(Requirement)).void }
17
+ def initialize(raw_version, requirement = nil)
18
+ super(
19
+ name: LANGUAGE,
20
+ version: Version.new(raw_version),
21
+ requirement: requirement
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ # NOTE: This file was scaffolded automatically but is OPTIONAL.
5
+ # If you don't need custom metadata finding logic (changelogs, release notes, etc.),
6
+ # you can safely delete this file and remove the require from lib/dependabot/bazel.rb
7
+
8
+ require "dependabot/metadata_finders"
9
+ require "dependabot/metadata_finders/base"
10
+
11
+ module Dependabot
12
+ module Bazel
13
+ class MetadataFinder < Dependabot::MetadataFinders::Base
14
+ extend T::Sig
15
+
16
+ private
17
+
18
+ sig { override.returns(T.nilable(Dependabot::Source)) }
19
+ def look_up_source
20
+ # TODO: Implement custom source lookup logic if needed
21
+ # Otherwise, delete this file and the require in the main registration file
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ Dependabot::MetadataFinders.register("bazel", Dependabot::Bazel::MetadataFinder)
@@ -0,0 +1,43 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/bazel/version"
6
+ require "dependabot/ecosystem"
7
+ require "dependabot/bazel/requirement"
8
+
9
+ module Dependabot
10
+ module Bazel
11
+ ECOSYSTEM = "bazel"
12
+ PACKAGE_MANAGER = "bazel"
13
+
14
+ # Keep versions in ascending order
15
+ SUPPORTED_BAZEL_VERSIONS = T.let([Version.new("6"), Version.new("7")].freeze, T::Array[Dependabot::Version])
16
+
17
+ # Currently, we don't support any deprecated versions of Bazel
18
+ # When a version is going to be unsupported, it will be added here for a while to give users time to upgrade
19
+ DEPRECATED_BAZEL_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
20
+
21
+ class PackageManager < Dependabot::Ecosystem::VersionManager
22
+ extend T::Sig
23
+
24
+ sig do
25
+ params(
26
+ detected_version: String,
27
+ raw_version: T.nilable(String),
28
+ requirement: T.nilable(Requirement)
29
+ ).void
30
+ end
31
+ def initialize(detected_version:, raw_version: nil, requirement: nil)
32
+ super(
33
+ name: PACKAGE_MANAGER,
34
+ detected_version: Version.new(detected_version),
35
+ version: raw_version ? Version.new(raw_version) : nil,
36
+ deprecated_versions: DEPRECATED_BAZEL_VERSIONS,
37
+ supported_versions: SUPPORTED_BAZEL_VERSIONS,
38
+ requirement: requirement
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,62 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/requirement"
6
+ require "dependabot/utils"
7
+
8
+ module Dependabot
9
+ module Bazel
10
+ class Requirement < Dependabot::Requirement
11
+ extend T::Sig
12
+
13
+ # Bazel dependencies typically use exact versions, not version ranges
14
+ # This class exists for consistency with Dependabot patterns but
15
+ # may not be heavily used since Bazel tends to pin exact versions
16
+
17
+ sig { params(requirement_string: String).returns(String) }
18
+ def self.normalize_requirement(requirement_string)
19
+ # Handle exact version specifications (most common in Bazel)
20
+ return requirement_string if requirement_string.match?(/^[<>=~]/)
21
+
22
+ # For bare version strings, treat as exact match
23
+ return "= #{requirement_string}" if requirement_string.match?(/^\d+(\.\d+)*(-[\w\d.]+)?(\+[\w\d.]+)?$/)
24
+
25
+ requirement_string
26
+ end
27
+
28
+ # This abstract method must be implemented
29
+ sig do
30
+ override
31
+ .params(requirement_string: T.nilable(String))
32
+ .returns(T::Array[Dependabot::Requirement])
33
+ end
34
+ def self.requirements_array(requirement_string)
35
+ # For Bazel, most requirements are simple exact versions
36
+ return [] if requirement_string.nil? || requirement_string.strip.empty?
37
+
38
+ normalized = normalize_requirement(requirement_string)
39
+ [new(normalized)]
40
+ end
41
+
42
+ sig { override.params(version: Gem::Version).returns(T::Boolean) }
43
+ def satisfied_by?(version)
44
+ # For Bazel versions, delegate to the base class
45
+ # but ensure we're working with proper version objects
46
+ bazel_version = case version
47
+ when Dependabot::Bazel::Version
48
+ version
49
+ else
50
+ Dependabot::Bazel::Version.new(version.to_s)
51
+ end
52
+
53
+ super(bazel_version)
54
+ rescue ArgumentError
55
+ false
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ Dependabot::Utils
62
+ .register_requirement_class("bazel", Dependabot::Bazel::Requirement)
@@ -0,0 +1,133 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "base64"
6
+ require "sorbet-runtime"
7
+ require "dependabot/shared_helpers"
8
+ require "dependabot/clients/github_with_retries"
9
+ require "dependabot/dependency"
10
+ require "dependabot/errors"
11
+
12
+ module Dependabot
13
+ module Bazel
14
+ class UpdateChecker
15
+ class RegistryClient
16
+ extend T::Sig
17
+
18
+ GITHUB_REPO = T.let("bazelbuild/bazel-central-registry", String)
19
+ RAW_BASE = T.let("https://raw.githubusercontent.com/#{GITHUB_REPO}/main".freeze, String)
20
+
21
+ sig { params(credentials: T::Array[Dependabot::Credential]).void }
22
+ def initialize(credentials:)
23
+ @credentials = credentials
24
+ end
25
+
26
+ sig { params(module_name: String).returns(T::Array[String]) }
27
+ def all_module_versions(module_name)
28
+ contents = T.unsafe(github_client).contents(GITHUB_REPO, path: "modules/#{module_name}")
29
+ return [] unless contents.is_a?(Array)
30
+
31
+ versions = contents.filter_map do |item|
32
+ next unless item[:type] == "dir"
33
+
34
+ item[:name]
35
+ end
36
+
37
+ versions.sort_by { |v| version_sort_key(v) }
38
+ rescue Octokit::NotFound
39
+ Dependabot.logger.info("Module '#{module_name}' not found in registry")
40
+ []
41
+ end
42
+
43
+ sig { params(module_name: String).returns(T.nilable(String)) }
44
+ def latest_module_version(module_name)
45
+ versions = all_module_versions(module_name)
46
+ return nil if versions.empty?
47
+
48
+ versions.max_by { |v| version_sort_key(v) }
49
+ end
50
+
51
+ sig { params(module_name: String).returns(T.nilable(T::Hash[String, T.untyped])) }
52
+ def get_metadata(module_name)
53
+ versions = all_module_versions(module_name)
54
+ return nil if versions.empty?
55
+
56
+ {
57
+ "name" => module_name,
58
+ "versions" => versions,
59
+ "latest_version" => latest_module_version(module_name)
60
+ }
61
+ end
62
+
63
+ sig { params(module_name: String, version: String).returns(T.nilable(T::Hash[String, T.untyped])) }
64
+ def get_source(module_name, version)
65
+ file_path = "modules/#{module_name}/#{version}/source.json"
66
+
67
+ begin
68
+ content = T.unsafe(github_client).contents(GITHUB_REPO, path: file_path)
69
+ return nil unless content
70
+
71
+ decoded_content = Base64.decode64(content.content)
72
+ JSON.parse(decoded_content)
73
+ rescue StandardError => e
74
+ Dependabot.logger.warn("Failed to get source for #{module_name}@#{version}: #{e.message}")
75
+ nil
76
+ end
77
+ end
78
+
79
+ sig { params(module_name: String, version: String).returns(T.nilable(String)) }
80
+ def get_module_bazel(module_name, version)
81
+ file_path = "modules/#{module_name}/#{version}/MODULE.bazel"
82
+
83
+ begin
84
+ content = T.unsafe(github_client).contents(GITHUB_REPO, path: file_path)
85
+ return nil unless content
86
+
87
+ Base64.decode64(content.content)
88
+ rescue StandardError => e
89
+ Dependabot.logger.warn("Failed to get MODULE.bazel for #{module_name}@#{version}: #{e.message}")
90
+ nil
91
+ end
92
+ end
93
+
94
+ sig { params(module_name: String, version: String).returns(T::Boolean) }
95
+ def module_version_exists?(module_name, version)
96
+ !get_source(module_name, version).nil?
97
+ end
98
+
99
+ sig { params(module_name: String, version: String).returns(T.nilable(Time)) }
100
+ def get_version_release_date(module_name, version)
101
+ file_path = "modules/#{module_name}/#{version}/MODULE.bazel"
102
+
103
+ commits = begin
104
+ T.unsafe(github_client).commits("bazelbuild/bazel-central-registry", path: file_path, per_page: 1)
105
+ rescue StandardError => e
106
+ Dependabot.logger.warn("Failed to get release date for #{module_name} #{version}: #{e.message}")
107
+ end
108
+
109
+ return nil unless commits&.any?
110
+
111
+ commits.first.commit.committer.date
112
+ end
113
+
114
+ private
115
+
116
+ sig { returns(Dependabot::Clients::GithubWithRetries) }
117
+ def github_client
118
+ @github_client ||= T.let(
119
+ Dependabot::Clients::GithubWithRetries.for_github_dot_com(credentials: @credentials),
120
+ T.nilable(Dependabot::Clients::GithubWithRetries)
121
+ )
122
+ end
123
+
124
+ sig { params(version: String).returns(T::Array[Integer]) }
125
+ def version_sort_key(version)
126
+ cleaned = version.gsub(/^v/, "")
127
+ parts = cleaned.split(".")
128
+ parts.map { |part| part.match?(/^\d+$/) ? part.to_i : 0 }
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,35 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module Dependabot
5
+ module Bazel
6
+ class UpdateChecker
7
+ class RequirementsUpdater
8
+ extend T::Sig
9
+
10
+ sig { params(requirements: T::Array[T::Hash[Symbol, T.untyped]], latest_version: String).void }
11
+ def initialize(requirements:, latest_version:)
12
+ @requirements = requirements
13
+ @latest_version = latest_version
14
+ end
15
+
16
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
17
+ def updated_requirements
18
+ @requirements.map do |requirement|
19
+ updated_requirement = requirement.dup
20
+ updated_requirement[:requirement] = @latest_version
21
+ updated_requirement
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
28
+ attr_reader :requirements
29
+
30
+ sig { returns(String) }
31
+ attr_reader :latest_version
32
+ end
33
+ end
34
+ end
35
+ end