dependabot-julia 0.345.0 → 0.347.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02efc3b82fb7c50bf4945d9d8fc1c182924291ade81c7d0cc1d3b6295fed571a
4
- data.tar.gz: 510ce71c0afa7f170f881367d5e97d7029b9169c2e28f8e70da5cd523ffe0b27
3
+ metadata.gz: 14960a6b1a395b60434d2ddc4aafec6dee23e5d49e1309d4b04f0f04818e9553
4
+ data.tar.gz: 19a4591beda7d2324c3f0d09404d7b2d3f4d9cdd80f149c153ba87a01b97285b
5
5
  SHA512:
6
- metadata.gz: 8d8e2c00e1714dfe6f7e904c34e9483133c676afb133791702232eff2271fd50864a10b806631a0902eca7635f28641b32d0b81aef5949cdc841425086f5ac00
7
- data.tar.gz: 35c245e66ef6a082d3e55b24ff9a014cec2cb3c67cfd59a8b29ed61e6d74bceb0f178401b152b5e2c18dd2c65ce33633ec8c8d2084770a1bda89b331166af681
6
+ metadata.gz: b4e63549d50c0163d37ad13847d653194a046c3734771b0d923153136dbfba163af83a46a8523ebf01034e0eaafabd67269a136d903702add09be953baacb482
7
+ data.tar.gz: a0bfd78e85d9a2146d64c412afe8302929d5a6ab5d722658edb665948819f868ddcf043e9f9a524e1458d95668e636df0ad79d09765f97b2a2f51621301078e7
@@ -3,6 +3,9 @@
3
3
 
4
4
  require "dependabot/file_fetchers"
5
5
  require "dependabot/file_fetchers/base"
6
+ require "dependabot/julia/registry_client"
7
+ require "dependabot/shared_helpers"
8
+ require "pathname"
6
9
 
7
10
  module Dependabot
8
11
  module Julia
@@ -22,38 +25,50 @@ module Dependabot
22
25
  # Julia is currently in beta - only fetch files if beta ecosystems are enabled
23
26
  return [] unless allow_beta_ecosystems?
24
27
 
25
- fetched_files = []
28
+ # Clone the repository temporarily to let Julia helper identify the correct files
29
+ SharedHelpers.in_a_temporary_repo_directory(directory, repo_contents_path) do |temp_dir|
30
+ fetch_files_using_julia_helper(temp_dir)
31
+ end
32
+ end
26
33
 
27
- # Fetch the main project file (Project.toml or JuliaProject.toml)
28
- fetched_files << project_file
34
+ private
29
35
 
30
- # Fetch the Manifest file if it exists
31
- fetched_files << manifest_file if manifest_file
36
+ sig { params(temp_dir: T.any(Pathname, String)).returns(T::Array[Dependabot::DependencyFile]) }
37
+ def fetch_files_using_julia_helper(temp_dir)
38
+ # Use Julia helper to identify the correct environment files
39
+ env_files = registry_client.find_environment_files(temp_dir.to_s)
32
40
 
33
- fetched_files
34
- end
41
+ if env_files.empty? || !env_files["project_file"]
42
+ raise Dependabot::DependencyFileNotFound, "No Project.toml or JuliaProject.toml found."
43
+ end
35
44
 
36
- private
45
+ fetched_files = []
37
46
 
38
- sig { returns(Dependabot::DependencyFile) }
39
- def project_file
40
- @project_file ||= T.let(
41
- fetch_file_if_present("Project.toml") ||
42
- fetch_file_if_present("JuliaProject.toml") ||
43
- raise(
44
- Dependabot::DependencyFileNotFound,
45
- "No Project.toml or JuliaProject.toml found."
46
- ),
47
- T.nilable(Dependabot::DependencyFile)
48
- )
47
+ # Fetch the project file identified by Julia helper
48
+ project_path = T.must(env_files["project_file"])
49
+ project_filename = File.basename(project_path)
50
+ fetched_files << fetch_file_from_host(project_filename)
51
+
52
+ # Fetch the manifest file if Julia helper found one
53
+ manifest_path = env_files["manifest_file"]
54
+ if manifest_path && !manifest_path.empty?
55
+ # Calculate relative path from project to manifest
56
+ project_dir = File.dirname(project_path)
57
+ manifest_relative = Pathname.new(manifest_path).relative_path_from(Pathname.new(project_dir)).to_s
58
+
59
+ # Fetch manifest (handles workspace cases where manifest is in parent directory)
60
+ manifest_file = fetch_file_if_present(manifest_relative)
61
+ fetched_files << manifest_file if manifest_file
62
+ end
63
+
64
+ fetched_files
49
65
  end
50
66
 
51
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
52
- def manifest_file
53
- @manifest_file ||= T.let(
54
- fetch_file_if_present("Manifest.toml") ||
55
- fetch_file_if_present("JuliaManifest.toml"),
56
- T.nilable(Dependabot::DependencyFile)
67
+ sig { returns(Dependabot::Julia::RegistryClient) }
68
+ def registry_client
69
+ @registry_client ||= T.let(
70
+ Dependabot::Julia::RegistryClient.new(credentials: credentials),
71
+ T.nilable(Dependabot::Julia::RegistryClient)
57
72
  )
58
73
  end
59
74
  end
@@ -1,7 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "toml-rb"
5
4
  require "tempfile"
6
5
  require "fileutils"
7
6
  require "dependabot/dependency"
@@ -35,8 +34,6 @@ module Dependabot
35
34
  options: {}
36
35
  )
37
36
  super
38
- @parsed_project_file = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
39
- @parsed_manifest_file = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
40
37
  @registry_client = T.let(nil, T.nilable(Dependabot::Julia::RegistryClient))
41
38
  @custom_registries = T.let(nil, T.nilable(T::Array[T::Hash[Symbol, T.untyped]]))
42
39
  @temp_dir = T.let(nil, T.nilable(String))
@@ -76,21 +73,11 @@ module Dependabot
76
73
  sig { returns(String) }
77
74
  def write_temp_project_file
78
75
  @temp_dir ||= Dir.mktmpdir("julia_project")
79
- project_path = File.join(@temp_dir, "Project.toml")
76
+ project_path = File.join(@temp_dir, T.must(project_file).name)
80
77
  File.write(project_path, T.must(project_file).content)
81
78
  project_path
82
79
  end
83
80
 
84
- sig { returns(T.nilable(String)) }
85
- def write_temp_manifest_file
86
- return nil unless manifest_file
87
-
88
- @temp_dir ||= Dir.mktmpdir("julia_project")
89
- manifest_path = File.join(@temp_dir, "Manifest.toml")
90
- File.write(manifest_path, T.must(manifest_file).content)
91
- manifest_path
92
- end
93
-
94
81
  sig { returns(T::Array[Dependabot::Dependency]) }
95
82
  def project_file_dependencies
96
83
  dependencies = T.let([], T::Array[Dependabot::Dependency])
@@ -98,22 +85,11 @@ module Dependabot
98
85
 
99
86
  # Use DependabotHelper.jl for project parsing
100
87
  project_path = write_temp_project_file
101
- manifest_path = write_temp_manifest_file if manifest_file
102
88
 
103
89
  begin
104
- result = registry_client.parse_project(
105
- project_path: project_path,
106
- manifest_path: manifest_path
107
- )
90
+ result = registry_client.parse_project(project_path: project_path)
108
91
 
109
- if result["error"]
110
- # Fallback to Ruby TOML parsing if Julia helper fails
111
- Dependabot.logger.warn(
112
- "DependabotHelper.jl parsing failed: #{result['error']}, " \
113
- "falling back to Ruby parsing"
114
- )
115
- return fallback_project_file_dependencies
116
- end
92
+ raise Dependabot::DependencyFileNotParseable, result["error"] if result["error"]
117
93
 
118
94
  # Convert DependabotHelper.jl result to Dependabot::Dependency objects
119
95
  dependencies = build_dependencies_from_julia_result(result)
@@ -128,104 +104,47 @@ module Dependabot
128
104
  sig { params(result: T::Hash[String, T.untyped]).returns(T::Array[Dependabot::Dependency]) }
129
105
  def build_dependencies_from_julia_result(result)
130
106
  dependencies = T.let([], T::Array[Dependabot::Dependency])
131
- parsed_deps = T.cast(result["dependencies"] || [], T::Array[T.untyped])
132
107
 
133
- parsed_deps.each do |dep_info|
134
- dep_hash = T.cast(dep_info, T::Hash[String, T.untyped])
135
- name = T.cast(dep_hash["name"], String)
136
- uuid = T.cast(dep_hash["uuid"], T.nilable(String))
137
- requirement_string = T.cast(dep_hash["requirement"] || "*", String)
138
- resolved_version = T.cast(dep_hash["resolved_version"], T.nilable(String))
108
+ # Process dependencies and weak dependencies (matching CompatHelper.jl behavior)
109
+ # Note: We don't process dev_dependencies/extras to match CompatHelper.jl
110
+ parsed_deps = T.cast(result["dependencies"] || [], T::Array[T.untyped])
111
+ dependencies.concat(build_dependencies_from_dep_list(parsed_deps, ["deps"]))
139
112
 
140
- # Skip Julia version requirement
141
- next if name == "julia"
142
-
143
- dependencies << Dependabot::Dependency.new(
144
- name: name,
145
- version: resolved_version,
146
- requirements: [{
147
- requirement: requirement_string,
148
- file: T.must(project_file).name,
149
- groups: ["runtime"],
150
- source: nil
151
- }],
152
- package_manager: "julia",
153
- metadata: uuid ? { julia_uuid: uuid } : {}
154
- )
155
- end
113
+ parsed_weak_deps = T.cast(result["weak_dependencies"] || [], T::Array[T.untyped])
114
+ dependencies.concat(build_dependencies_from_dep_list(parsed_weak_deps, ["weakdeps"]))
156
115
 
157
116
  dependencies
158
117
  end
159
118
 
160
- # Fallback method using Ruby TOML parsing
161
- sig { returns(T::Array[Dependabot::Dependency]) }
162
- def fallback_project_file_dependencies
163
- dependencies = T.let([], T::Array[Dependabot::Dependency])
164
-
165
- parsed_project = parsed_project_file
166
- deps_section = T.cast(parsed_project["deps"] || {}, T::Hash[String, T.untyped])
167
- compat_section = T.cast(parsed_project["compat"] || {}, T::Hash[String, T.untyped])
168
-
169
- deps_section.each do |name, _uuid|
119
+ sig do
120
+ params(
121
+ dep_list: T::Array[T.untyped],
122
+ groups: T::Array[String]
123
+ ).returns(T::Array[Dependabot::Dependency])
124
+ end
125
+ def build_dependencies_from_dep_list(dep_list, groups)
126
+ dep_list.filter_map do |dep_info|
127
+ dep_hash = T.cast(dep_info, T::Hash[String, T.untyped])
128
+ name = T.cast(dep_hash["name"], String)
170
129
  next if name == "julia" # Skip Julia version requirement
171
130
 
172
- # Get the version requirement from compat section, default to "*" if not specified
173
- requirement_string = T.cast(compat_section[name] || "*", String)
174
-
175
- # Get the exact version from Manifest.toml if available
176
- exact_version = version_from_manifest(name)
131
+ uuid = T.cast(dep_hash["uuid"], T.nilable(String))
132
+ # NOTE: Missing "requirement" means no compat entry (any version acceptable)
133
+ requirement_string = T.cast(dep_hash["requirement"], T.nilable(String))
177
134
 
178
- dependencies << Dependabot::Dependency.new(
135
+ Dependabot::Dependency.new(
179
136
  name: name,
180
- version: exact_version,
137
+ version: nil, # Julia dependencies don't use locked versions
181
138
  requirements: [{
182
139
  requirement: requirement_string,
183
140
  file: T.must(project_file).name,
184
- groups: ["runtime"],
141
+ groups: groups,
185
142
  source: nil
186
143
  }],
187
- package_manager: "julia"
144
+ package_manager: "julia",
145
+ metadata: uuid ? { julia_uuid: uuid } : {}
188
146
  )
189
147
  end
190
-
191
- dependencies
192
- end
193
-
194
- sig { params(dependency_name: String).returns(T.nilable(String)) }
195
- def version_from_manifest(dependency_name)
196
- return nil unless manifest_file
197
-
198
- # Try using DependabotHelper.jl first
199
- temp_dir = Dir.mktmpdir("julia_manifest_only")
200
- manifest_path = File.join(temp_dir, "Manifest.toml")
201
- File.write(manifest_path, T.must(manifest_file).content)
202
-
203
- begin
204
- # We need the UUID for the DependabotHelper.jl call, so fallback to Ruby parsing
205
- # Note: Future enhancement could add name-only lookup to DependabotHelper.jl
206
- fallback_version_from_manifest(dependency_name)
207
- ensure
208
- FileUtils.rm_rf(temp_dir)
209
- end
210
- end
211
-
212
- sig { params(dependency_name: String).returns(T.nilable(String)) }
213
- def fallback_version_from_manifest(dependency_name)
214
- return nil unless manifest_file
215
-
216
- parsed_manifest = parsed_manifest_file
217
-
218
- # Look for the dependency in the manifest
219
- deps_section = T.cast(parsed_manifest["deps"], T.nilable(T::Hash[String, T.untyped]))
220
- if deps_section && deps_section[dependency_name]
221
- # Manifest v2 format
222
- dep_entries = deps_section[dependency_name]
223
- if dep_entries.is_a?(Array) && dep_entries.first.is_a?(Hash)
224
- return T.cast(dep_entries.first["version"], T.nilable(String))
225
- end
226
- end
227
-
228
- nil
229
148
  end
230
149
 
231
150
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
@@ -236,37 +155,6 @@ module Dependabot
236
155
  )
237
156
  end
238
157
 
239
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
240
- def manifest_file
241
- @manifest_file ||= T.let(
242
- get_original_file("Manifest.toml") || get_original_file("JuliaManifest.toml"),
243
- T.nilable(Dependabot::DependencyFile)
244
- )
245
- end
246
-
247
- sig { returns(T::Hash[String, T.untyped]) }
248
- def parsed_project_file
249
- return @parsed_project_file if @parsed_project_file
250
-
251
- parsed_content = T.cast(TomlRB.parse(T.must(project_file).content), T::Hash[String, T.untyped])
252
- @parsed_project_file = parsed_content
253
- parsed_content
254
- rescue TomlRB::ParseError, TomlRB::ValueOverwriteError => e
255
- raise Dependabot::DependencyFileNotParseable, "Error parsing #{T.must(project_file).name}: #{e.message}"
256
- end
257
-
258
- sig { returns(T::Hash[String, T.untyped]) }
259
- def parsed_manifest_file
260
- return {} unless manifest_file
261
- return @parsed_manifest_file if @parsed_manifest_file
262
-
263
- parsed_content = T.cast(TomlRB.parse(T.must(manifest_file).content), T::Hash[String, T.untyped])
264
- @parsed_manifest_file = parsed_content
265
- parsed_content
266
- rescue TomlRB::ParseError, TomlRB::ValueOverwriteError => e
267
- raise Dependabot::DependencyFileNotParseable, "Error parsing #{T.must(manifest_file).name}: #{e.message}"
268
- end
269
-
270
158
  sig { override.void }
271
159
  def check_required_files
272
160
  raise "No Project.toml or JuliaProject.toml!" unless project_file
@@ -3,9 +3,12 @@
3
3
 
4
4
  require "toml-rb"
5
5
  require "tempfile"
6
+ require "fileutils"
7
+ require "pathname"
6
8
  require "dependabot/file_updaters"
7
9
  require "dependabot/file_updaters/base"
8
10
  require "dependabot/julia/registry_client"
11
+ require "dependabot/notices"
9
12
 
10
13
  module Dependabot
11
14
  module Julia
@@ -17,72 +20,51 @@ module Dependabot
17
20
  [/(?:Julia)?Project\.toml$/i, /(?:Julia)?Manifest(?:-v[\d.]+)?\.toml$/i]
18
21
  end
19
22
 
23
+ sig { override.returns(T::Array[Dependabot::Notice]) }
24
+ attr_reader :notices
25
+
26
+ sig do
27
+ override.params(
28
+ dependencies: T::Array[Dependabot::Dependency],
29
+ dependency_files: T::Array[Dependabot::DependencyFile],
30
+ credentials: T::Array[Dependabot::Credential],
31
+ repo_contents_path: T.nilable(String),
32
+ options: T::Hash[Symbol, T.untyped]
33
+ ).void
34
+ end
35
+ def initialize(dependencies:, dependency_files:, credentials:, repo_contents_path: nil, options: {})
36
+ super
37
+ @notices = T.let([], T::Array[Dependabot::Notice])
38
+ end
39
+
20
40
  sig { override.returns(T::Array[Dependabot::DependencyFile]) }
21
41
  def updated_dependency_files
22
- updated_files = []
42
+ # If no project file, cannot proceed
43
+ raise "No Project.toml file found" unless project_file
23
44
 
24
45
  # Use DependabotHelper.jl for manifest updating
25
- if project_file
26
- project_path = T.let(nil, T.nilable(String))
27
-
28
- begin
29
- project_path = write_temp_project_file
30
-
31
- result = registry_client.update_manifest(
32
- project_path: project_path,
33
- updates: build_updates_hash
34
- )
35
-
36
- if result["error"]
37
- # Fallback to Ruby TOML manipulation
38
- Dependabot.logger.warn(
39
- "DependabotHelper.jl update failed: #{result['error']}, " \
40
- "falling back to Ruby updating"
41
- )
42
- return fallback_updated_dependency_files
43
- end
44
-
45
- # Create updated files from DependabotHelper.jl results
46
- updated_files = build_updated_files_from_result(result)
47
- rescue StandardError => e
48
- # Fallback to Ruby TOML manipulation if Julia helper fails
49
- Dependabot.logger.warn(
50
- "DependabotHelper.jl update failed with exception: #{e.message}, " \
51
- "falling back to Ruby updating"
52
- )
53
- return fallback_updated_dependency_files
54
- ensure
55
- File.delete(project_path) if project_path && File.exist?(project_path)
56
- end
57
- end
58
-
59
- raise "No files changed!" if updated_files.empty?
60
-
61
- updated_files
46
+ # This works for both standard packages and workspace packages
47
+ updated_files_with_julia_helper
62
48
  end
63
49
 
64
- # Fallback method using Ruby TOML manipulation
65
50
  sig { returns(T::Array[Dependabot::DependencyFile]) }
66
- def fallback_updated_dependency_files
51
+ def updated_files_with_julia_helper
67
52
  updated_files = []
68
53
 
69
- # Update Project.toml file
70
- if project_file && file_changed?(T.must(project_file))
71
- updated_files << updated_file(
72
- file: T.must(project_file),
73
- content: updated_project_content
74
- )
75
- end
54
+ SharedHelpers.in_a_temporary_repo_directory(T.must(dependency_files.first).directory, repo_contents_path) do
55
+ updated_project = updated_project_content
56
+ actual_manifest = find_manifest_file
76
57
 
77
- # Update Manifest.toml file if it exists and dependencies have changed
78
- if manifest_file
79
- updated_manifest_content = build_updated_manifest_content
80
- if updated_manifest_content != T.must(manifest_file).content
81
- updated_files << updated_file(
82
- file: T.must(manifest_file),
83
- content: updated_manifest_content
84
- )
85
- end
58
+ return project_only_update(updated_project) if actual_manifest.nil?
59
+
60
+ # Work directly in the repo directory - no need for another temp directory
61
+ # This ensures all workspace packages are accessible to Julia's Pkg
62
+ write_temporary_files(updated_project, actual_manifest)
63
+ result = call_julia_helper
64
+
65
+ return handle_julia_helper_error(result, actual_manifest, updated_project) if result["error"]
66
+
67
+ build_updated_files(updated_files, updated_project, actual_manifest, result)
86
68
  end
87
69
 
88
70
  raise "No files changed!" if updated_files.empty?
@@ -90,6 +72,111 @@ module Dependabot
90
72
  updated_files
91
73
  end
92
74
 
75
+ sig { params(updated_project: String).returns(T::Array[Dependabot::DependencyFile]) }
76
+ def project_only_update(updated_project)
77
+ [updated_file(file: T.must(project_file), content: updated_project)]
78
+ end
79
+
80
+ sig do
81
+ params(
82
+ updated_project: String,
83
+ actual_manifest: Dependabot::DependencyFile
84
+ ).void
85
+ end
86
+ def write_temporary_files(updated_project, actual_manifest)
87
+ File.write(T.must(project_file).name, updated_project)
88
+
89
+ # Preserve relative paths (e.g., ../Manifest.toml for workspace packages)
90
+ # so Julia's Pkg can find and update the correct shared manifest
91
+ manifest_path = actual_manifest.name
92
+ FileUtils.mkdir_p(File.dirname(manifest_path)) if manifest_path.include?("/")
93
+ File.write(manifest_path, actual_manifest.content)
94
+ end
95
+
96
+ sig { returns(T::Hash[String, T.untyped]) }
97
+ def call_julia_helper
98
+ registry_client.update_manifest(
99
+ project_path: Dir.pwd,
100
+ updates: build_updates_hash
101
+ )
102
+ end
103
+
104
+ sig do
105
+ params(
106
+ result: T::Hash[String, T.untyped],
107
+ actual_manifest: Dependabot::DependencyFile,
108
+ updated_project: String
109
+ ).returns(T::Array[Dependabot::DependencyFile])
110
+ end
111
+ def handle_julia_helper_error(result, actual_manifest, updated_project)
112
+ error_message = result["error"]
113
+ manifest_path = actual_manifest.name
114
+
115
+ is_resolver_error = resolver_error?(error_message)
116
+ raise error_message unless is_resolver_error
117
+
118
+ add_manifest_update_notice(manifest_path, error_message)
119
+
120
+ # Return only the updated Project.toml
121
+ [updated_file(file: T.must(project_file), content: updated_project)]
122
+ end
123
+
124
+ sig { params(error_message: String).returns(T::Boolean) }
125
+ def resolver_error?(error_message)
126
+ error_message.start_with?("Pkg resolver error:") ||
127
+ error_message.include?("Unsatisfiable requirements") ||
128
+ error_message.include?("ResolverError")
129
+ end
130
+
131
+ sig { params(manifest_path: String, error_message: String).void }
132
+ def add_manifest_update_notice(manifest_path, error_message)
133
+ # Resolve relative paths to absolute paths for clarity in user-facing notices
134
+ # Use Pathname.cleanpath to handle any depth of relative paths (e.g., ../../Manifest.toml)
135
+ project_dir = T.must(project_file).directory
136
+ absolute_manifest_path = if manifest_path.start_with?("../", "./")
137
+ # For workspace packages, compute the absolute path
138
+ Pathname.new(File.join(project_dir, manifest_path)).cleanpath.to_s
139
+ else
140
+ # For regular packages, use the manifest path as-is
141
+ File.join(project_dir, manifest_path)
142
+ end
143
+
144
+ @notices << Dependabot::Notice.new(
145
+ mode: Dependabot::Notice::NoticeMode::WARN,
146
+ type: "julia_manifest_not_updated",
147
+ package_manager_name: "Pkg",
148
+ title: "Could not update manifest #{absolute_manifest_path}",
149
+ description: "The Julia package manager failed to update the new dependency versions " \
150
+ "in `#{absolute_manifest_path}`:\n\n```\n#{error_message}\n```",
151
+ show_in_pr: true,
152
+ show_alert: true
153
+ )
154
+ end
155
+
156
+ sig do
157
+ params(
158
+ updated_files: T::Array[Dependabot::DependencyFile],
159
+ updated_project: String,
160
+ actual_manifest: Dependabot::DependencyFile,
161
+ result: T::Hash[String, T.untyped]
162
+ ).void
163
+ end
164
+ def build_updated_files(updated_files, updated_project, actual_manifest, result)
165
+ updated_files << updated_file(file: T.must(project_file), content: updated_project)
166
+
167
+ return unless result["manifest_content"]
168
+
169
+ updated_manifest_content = result["manifest_content"]
170
+ return unless updated_manifest_content != actual_manifest.content
171
+
172
+ manifest_for_update = if result["manifest_path"]
173
+ manifest_file_for_path(result["manifest_path"])
174
+ else
175
+ actual_manifest
176
+ end
177
+ updated_files << updated_file(file: manifest_for_update, content: updated_manifest_content)
178
+ end
179
+
93
180
  private
94
181
 
95
182
  sig { returns(T::Hash[String, String]) }
@@ -98,33 +185,15 @@ module Dependabot
98
185
  dependencies.each do |dependency|
99
186
  next unless dependency.version
100
187
 
101
- updates[dependency.name] = dependency.version
188
+ uuid = T.cast(dependency.metadata[:julia_uuid], String)
189
+ updates[uuid] = {
190
+ "name" => dependency.name,
191
+ "version" => dependency.version
192
+ }
102
193
  end
103
194
  updates
104
195
  end
105
196
 
106
- sig { params(result: T::Hash[String, T.untyped]).returns(T::Array[Dependabot::DependencyFile]) }
107
- def build_updated_files_from_result(result)
108
- updated_files = T.let([], T::Array[Dependabot::DependencyFile])
109
-
110
- if result["project_content"] && result["project_content"] != T.must(project_file).content
111
- updated_files << updated_file(
112
- file: T.must(project_file),
113
- content: result["project_content"]
114
- )
115
- end
116
-
117
- if manifest_file && result["manifest_content"] &&
118
- result["manifest_content"] != T.must(manifest_file).content
119
- updated_files << updated_file(
120
- file: T.must(manifest_file),
121
- content: result["manifest_content"]
122
- )
123
- end
124
-
125
- updated_files
126
- end
127
-
128
197
  # Helper methods for DependabotHelper.jl integration
129
198
 
130
199
  sig { returns(Dependabot::Julia::RegistryClient) }
@@ -137,12 +206,54 @@ module Dependabot
137
206
  )
138
207
  end
139
208
 
140
- sig { returns(String) }
141
- def write_temp_project_file
142
- temp_file = Tempfile.new(["Project", ".toml"])
143
- temp_file.write(T.must(project_file).content)
144
- temp_file.close
145
- T.must(temp_file.path)
209
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
210
+ def find_manifest_file
211
+ # The file fetcher has already identified the correct manifest file
212
+ # For regular packages: manifest in same directory
213
+ # For workspace packages: manifest in parent directory
214
+ # We just need to find it in dependency_files
215
+ project_dir = T.must(project_file).directory
216
+
217
+ dependency_files.find do |f|
218
+ # Use basename to get just the filename, not the full path with ../
219
+ is_manifest = File.basename(f.name).match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
220
+ is_manifest && (f.directory == project_dir || project_dir.start_with?(f.directory))
221
+ end
222
+ end
223
+
224
+ sig { params(manifest_path: String).returns(Dependabot::DependencyFile) }
225
+ def manifest_file_for_path(manifest_path)
226
+ # manifest_path is relative to the project directory (e.g., "Manifest.toml" or "../Manifest.toml")
227
+ # We need to resolve it to find the actual manifest file in dependency_files
228
+
229
+ # Build the absolute path relative to the project directory
230
+ project_dir = T.must(project_file).directory
231
+ resolved_manifest_path = File.expand_path(manifest_path, project_dir)
232
+
233
+ # Normalize by removing leading "/" to get repo-relative path
234
+ resolved_manifest_path = resolved_manifest_path.sub(%r{^/}, "")
235
+
236
+ # Find the matching manifest file in dependency_files
237
+ found_manifest = dependency_files.find do |f|
238
+ next unless f.name.match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
239
+
240
+ # Construct the full path for this file and normalize it
241
+ file_path = File.join(f.directory, f.name).sub(%r{^/}, "")
242
+ file_path == resolved_manifest_path
243
+ end
244
+
245
+ # If we found the manifest file and the manifest_path matches its name exactly,
246
+ # return the original file to preserve its metadata
247
+ return found_manifest if found_manifest && found_manifest.name == manifest_path
248
+
249
+ # For workspace cases where manifest_path is relative (e.g., "../Manifest.toml"),
250
+ # we need to create a new DependencyFile with the relative path as its name,
251
+ # but copy the content from the found manifest if it exists
252
+ Dependabot::DependencyFile.new(
253
+ name: manifest_path,
254
+ content: found_manifest&.content || "",
255
+ directory: project_dir
256
+ )
146
257
  end
147
258
 
148
259
  sig { override.void }
@@ -164,16 +275,6 @@ module Dependabot
164
275
  )
165
276
  end
166
277
 
167
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
168
- def manifest_file
169
- @manifest_file ||= T.let(
170
- dependency_files.find do |f|
171
- f.name.match?(/^(Julia)?Manifest(?:-v[\d.]+)?\.toml$/i)
172
- end,
173
- T.nilable(Dependabot::DependencyFile)
174
- )
175
- end
176
-
177
278
  sig { returns(String) }
178
279
  def updated_project_content
179
280
  return T.must(T.must(project_file).content) unless project_file
@@ -196,15 +297,24 @@ module Dependabot
196
297
 
197
298
  sig { params(content: String, dependency_name: String, new_requirement: String).returns(String) }
198
299
  def update_dependency_requirement_in_content(content, dependency_name, new_requirement)
199
- # Pattern to match the dependency in [compat] section
200
- # Handles various quote styles and spacing
201
- pattern = /(^\s*#{Regexp.escape(dependency_name)}\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s#\n]+)(\s*(?:\#.*)?$)/mx
202
-
203
- if content.match?(pattern)
204
- # Replace existing entry
205
- content.gsub(pattern, "\\1\"#{new_requirement}\"\\2")
300
+ # Extract the [compat] section to update it specifically
301
+ compat_section_match = content.match(/^\[compat\]\s*\n((?:(?!\[)[^\n]*\n)*?)(?=^\[|\z)/m)
302
+
303
+ if compat_section_match
304
+ compat_section = T.must(compat_section_match[1])
305
+ # Pattern to match the dependency in the compat section
306
+ pattern = /^(\s*#{Regexp.escape(dependency_name)}\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s#\n]+)(\s*(?:\#.*)?)$/
307
+
308
+ if compat_section.match?(pattern)
309
+ # Replace existing entry in compat section
310
+ updated_compat = compat_section.gsub(pattern, "\\1\"#{new_requirement}\"\\2")
311
+ content.sub(T.must(compat_section_match[0]), "[compat]\n#{updated_compat}")
312
+ else
313
+ # Add new entry to existing [compat] section
314
+ add_compat_entry_to_content(content, dependency_name, new_requirement)
315
+ end
206
316
  else
207
- # Add new entry to [compat] section
317
+ # Add new [compat] section
208
318
  add_compat_entry_to_content(content, dependency_name, new_requirement)
209
319
  end
210
320
  end
@@ -220,74 +330,6 @@ module Dependabot
220
330
  content + "\n[compat]\n#{dependency_name} = \"#{requirement}\"\n"
221
331
  end
222
332
  end
223
-
224
- sig { returns(String) }
225
- def build_updated_manifest_content
226
- return T.must(T.must(manifest_file).content) unless manifest_file
227
-
228
- content = T.must(T.must(manifest_file).content)
229
-
230
- dependencies.each do |dependency|
231
- next unless dependency.version
232
-
233
- content = update_dependency_version_in_manifest(content, dependency.name, T.must(dependency.version))
234
- end
235
-
236
- content
237
- end
238
-
239
- sig { params(content: String, dependency_name: String, new_version: String).returns(String) }
240
- def update_dependency_version_in_manifest(content, dependency_name, new_version)
241
- # Pattern to find the dependency entry and update its version
242
- # Matches the [[deps.DependencyName]] section and updates the version line within it
243
- dep_start = /^\[\[deps\.#{Regexp.escape(dependency_name)}\]\]\s*\n(?:.*\n)*?/
244
- version_key = /^\s*version\s*=\s*/
245
- old_version = /(?:"[^"]*"|'[^']*'|[^\s#\n]+)/
246
- trailing = /\s*(?:\#.*)?$/
247
- pattern = /(#{dep_start})(#{version_key})#{old_version}(#{trailing})/mx
248
-
249
- if content.match?(pattern)
250
- content.gsub(pattern, "\\1\\2\"#{new_version}\"\\3")
251
- else
252
- # If pattern doesn't match, fall back to original approach
253
- Dependabot.logger.warn("Could not find manifest entry for #{dependency_name}, using fallback")
254
- fallback_update_manifest_content(content, dependency_name, new_version)
255
- end
256
- end
257
-
258
- sig { params(content: String, dependency_name: String, new_version: String).returns(String) }
259
- def fallback_update_manifest_content(content, dependency_name, new_version)
260
- # Fallback to parse-and-dump for complex cases
261
- parsed_manifest = T.cast(TomlRB.parse(content), T::Hash[String, T.untyped])
262
-
263
- deps_section = T.cast(parsed_manifest["deps"] || {}, T::Hash[String, T.untyped])
264
- if deps_section[dependency_name]
265
- dep_entries = deps_section[dependency_name]
266
- update_dependency_entries(dep_entries, new_version)
267
- end
268
-
269
- T.cast(TomlRB.dump(parsed_manifest), String)
270
- end
271
-
272
- sig { params(dependency: Dependabot::Dependency, manifest: T::Hash[String, T.untyped]).void }
273
- def update_dependency_in_manifest(dependency, manifest)
274
- deps_section = T.cast(manifest["deps"] || {}, T::Hash[String, T.untyped])
275
- return unless deps_section[dependency.name]
276
-
277
- dep_entries = deps_section[dependency.name]
278
- update_dependency_entries(dep_entries, dependency.version)
279
- end
280
-
281
- sig { params(dep_entries: T.untyped, version: T.nilable(String)).void }
282
- def update_dependency_entries(dep_entries, version)
283
- if dep_entries.is_a?(Array)
284
- dep_entries.each do |dep_entry|
285
- dep_entry["version"] = version if dep_entry.is_a?(Hash) && dep_entry["uuid"]
286
- end
287
- elsif dep_entries.is_a?(Hash) && dep_entries["uuid"]
288
- dep_entries["version"] = version
289
- end
290
- end
291
333
  end
292
334
  end
293
335
  end
@@ -1,13 +1,10 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- require "excon"
5
4
  require "dependabot/metadata_finders"
6
5
  require "dependabot/metadata_finders/base"
7
- require "dependabot/registry_client"
8
6
  require "dependabot/julia/registry_client"
9
7
  require "uri" # Required for URI.parse
10
- require "toml-rb" # Required for TOML parsing
11
8
 
12
9
  module Dependabot
13
10
  module Julia
@@ -21,9 +18,8 @@ module Dependabot
21
18
 
22
19
  sig { override.returns(T.nilable(Dependabot::Source)) }
23
20
  def look_up_source
24
- # Only use authoritative sources from Julia helper or dependency files
25
- url_string = source_url_from_julia_helper ||
26
- source_url_from_dependency_files
21
+ # Only use authoritative sources from Julia helper
22
+ url_string = source_url_from_julia_helper
27
23
 
28
24
  return nil unless url_string
29
25
 
@@ -84,14 +80,6 @@ module Dependabot
84
80
  Dependabot.logger.error("Invalid URI for dependency #{dependency.name}: #{url_string} - #{e.message}")
85
81
  nil
86
82
  end
87
-
88
- sig { returns(T.nilable(String)) }
89
- def source_url_from_dependency_files
90
- # MetadataFinder doesn't have access to dependency_files
91
- # This would typically be handled by FileParser or other components
92
- # For now, we'll skip this strategy and rely on the Julia helper
93
- nil
94
- end
95
83
  end
96
84
  end
97
85
  end
@@ -1,4 +1,4 @@
1
- # typed: strong
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "time"
@@ -72,31 +72,57 @@ module Dependabot
72
72
  ).returns(T::Array[Dependabot::Package::PackageRelease])
73
73
  end
74
74
  def build_releases_for_versions(registry_client, available_versions, uuid)
75
- releases = T.let([], T::Array[Dependabot::Package::PackageRelease])
75
+ # Use batch operation to fetch all release dates at once
76
+ release_dates = fetch_release_dates_batch(registry_client, available_versions, uuid)
76
77
 
77
- available_versions.each do |version_string|
78
+ available_versions.map do |version_string|
78
79
  version = Julia::Version.new(version_string)
79
- release_date = fetch_release_date_safely(registry_client, version_string, uuid)
80
+ release_date = release_dates[version_string]
80
81
 
81
- releases << create_package_release(version, release_date)
82
+ create_package_release(version, release_date)
82
83
  end
83
-
84
- releases
85
84
  end
86
85
 
87
86
  sig do
88
87
  params(
89
88
  registry_client: RegistryClient,
90
- version_string: String,
89
+ available_versions: T::Array[String],
91
90
  uuid: T.nilable(String)
92
- ).returns(T.nilable(Time))
91
+ ).returns(T::Hash[String, T.nilable(Time)])
93
92
  end
94
- def fetch_release_date_safely(registry_client, version_string, uuid)
95
- registry_client.fetch_version_release_date(dependency.name, version_string, uuid)
96
- rescue StandardError => e
97
- Dependabot.logger.warn(
98
- "Failed to fetch release info for #{dependency.name} version #{version_string}: #{e.message}"
99
- )
93
+ def fetch_release_dates_batch(registry_client, available_versions, uuid)
94
+ return {} if available_versions.empty?
95
+
96
+ packages_versions = [{
97
+ name: dependency.name,
98
+ uuid: uuid || "",
99
+ versions: available_versions
100
+ }]
101
+
102
+ result = registry_client.batch_fetch_version_release_dates(packages_versions)
103
+ dates_for_package = result[dependency.name] || {}
104
+
105
+ convert_dates_to_time_objects(dates_for_package)
106
+ end
107
+
108
+ sig do
109
+ params(
110
+ dates_hash: T::Hash[String, T.untyped]
111
+ ).returns(T::Hash[String, T.nilable(Time)])
112
+ end
113
+ def convert_dates_to_time_objects(dates_hash)
114
+ dates_hash.transform_values do |date_value|
115
+ convert_single_date(date_value)
116
+ end
117
+ end
118
+
119
+ sig { params(date_value: T.untyped).returns(T.nilable(Time)) }
120
+ def convert_single_date(date_value)
121
+ return nil if date_value.nil?
122
+ return nil if date_value.is_a?(Hash) && date_value["error"]
123
+
124
+ Time.parse(date_value.to_s)
125
+ rescue ArgumentError, TypeError
100
126
  nil
101
127
  end
102
128
 
@@ -14,10 +14,10 @@ module Dependabot
14
14
  PACKAGE_MANAGER_COMMAND = T.let("julia", String)
15
15
  # Julia versions as of June 2025:
16
16
  # - 1.10 is the LTS (Long Term Support) version
17
- # - 1.11 is the current stable version
17
+ # - 1.12 is the current stable version
18
18
  # Update these constants when new LTS or major versions are released
19
19
  MINIMUM_VERSION = T.let("1.10", String) # LTS version
20
- CURRENT_VERSION = T.let("1.11", String) # Current stable version
20
+ CURRENT_VERSION = T.let("1.12", String) # Current stable version
21
21
 
22
22
  sig { returns(T.nilable(String)) }
23
23
  def self.detected_version
@@ -87,24 +87,6 @@ module Dependabot
87
87
  nil
88
88
  end
89
89
 
90
- sig do
91
- params(
92
- project_path: String,
93
- package_name: String,
94
- target_version: String
95
- ).returns(T::Hash[String, T.untyped])
96
- end
97
- def check_update_compatibility(project_path, package_name, target_version)
98
- call_julia_helper(
99
- function: "check_update_compatibility",
100
- args: {
101
- project_path: project_path,
102
- package_name: package_name,
103
- target_version: target_version
104
- }
105
- )
106
- end
107
-
108
90
  sig do
109
91
  params(
110
92
  project_path: String,
@@ -129,6 +111,24 @@ module Dependabot
129
111
  )
130
112
  end
131
113
 
114
+ sig { params(directory: String).returns(T::Hash[String, String]) }
115
+ def find_environment_files(directory)
116
+ result = call_julia_helper(
117
+ function: "find_environment_files",
118
+ args: { directory: directory }
119
+ )
120
+
121
+ return {} if result["error"]
122
+
123
+ {
124
+ "project_file" => result["project_file"],
125
+ "manifest_file" => result["manifest_file"]
126
+ }
127
+ rescue StandardError => e
128
+ Dependabot.logger.warn("Failed to find environment files in #{directory}: #{e.message}")
129
+ {}
130
+ end
131
+
132
132
  sig do
133
133
  params(
134
134
  manifest_path: String,
@@ -160,22 +160,6 @@ module Dependabot
160
160
  )
161
161
  end
162
162
 
163
- sig do
164
- params(
165
- package_name: String,
166
- source_url: String
167
- ).returns(T::Hash[String, T.untyped])
168
- end
169
- def extract_package_metadata_from_url(package_name, source_url)
170
- call_julia_helper(
171
- function: "extract_package_metadata_from_url",
172
- args: {
173
- package_name: package_name,
174
- source_url: source_url
175
- }
176
- )
177
- end
178
-
179
163
  sig do
180
164
  params(
181
165
  project_path: String,
@@ -269,6 +253,68 @@ module Dependabot
269
253
  []
270
254
  end
271
255
 
256
+ # ============================================================================
257
+ # BATCH OPERATIONS
258
+ # ============================================================================
259
+
260
+ sig { params(dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, T.untyped]) }
261
+ def batch_fetch_package_info(dependencies)
262
+ packages = dependencies.map do |dep|
263
+ {
264
+ name: dep.name,
265
+ uuid: dep.metadata[:julia_uuid] || ""
266
+ }
267
+ end
268
+
269
+ call_julia_helper(
270
+ function: "batch_get_package_info",
271
+ args: { packages: packages }
272
+ )
273
+ rescue StandardError => e
274
+ Dependabot.logger.error("Failed to batch fetch package info: #{e.message}")
275
+ {}
276
+ end
277
+
278
+ sig do
279
+ params(
280
+ packages_versions: T::Array[T::Hash[Symbol, T.untyped]]
281
+ ).returns(T::Hash[String, T::Hash[String, T.nilable(String)]])
282
+ end
283
+ def batch_fetch_version_release_dates(packages_versions)
284
+ result = call_julia_helper(
285
+ function: "batch_get_version_release_dates",
286
+ args: { packages_versions: packages_versions }
287
+ )
288
+
289
+ # Convert the result to a more Ruby-friendly format
290
+ result.transform_values do |dates|
291
+ next dates if dates.is_a?(Hash) && dates["error"]
292
+
293
+ dates.is_a?(Hash) ? dates : {}
294
+ end
295
+ rescue StandardError => e
296
+ Dependabot.logger.error("Failed to batch fetch version release dates: #{e.message}")
297
+ {}
298
+ end
299
+
300
+ sig { params(dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, T.untyped]) }
301
+ def batch_fetch_available_versions(dependencies)
302
+ packages = dependencies.map do |dep|
303
+ {
304
+ name: dep.name,
305
+ uuid: dep.metadata[:julia_uuid] || ""
306
+ }
307
+ end
308
+
309
+ call_julia_helper(
310
+ function: "batch_get_available_versions",
311
+ args: { packages: packages }
312
+ )
313
+ rescue StandardError => e
314
+ Dependabot.logger.error("Failed to batch fetch available versions: #{e.message}")
315
+ {}
316
+ end
317
+
272
318
  private
273
319
 
274
320
  sig { returns(T::Array[T::Hash[Symbol, String]]) }
@@ -14,16 +14,17 @@ module Dependabot
14
14
  # - Range: "1.2-1.3", ">=1.0, <2.0"
15
15
  # - Caret: "^1.2" (compatible within major version)
16
16
  # - Tilde: "~1.2.3" (compatible within minor version)
17
- # - Wildcard: "*" (any version)
18
- return [new(">= 0")] if requirement_string.nil? || requirement_string.empty? || requirement_string == "*"
17
+ # Note: Missing compat entry (nil/empty) means any version is acceptable
18
+ return [new(">= 0")] if requirement_string.nil? || requirement_string.empty?
19
19
 
20
20
  # Split by comma for multiple constraints
21
21
  constraints = requirement_string.split(",").map(&:strip)
22
22
 
23
23
  constraints.map do |constraint|
24
- # Handle Julia-specific patterns
25
- normalized_constraint = normalize_julia_constraint(constraint)
26
- new(normalized_constraint)
24
+ # Handle Julia-specific patterns - returns an array of gem requirement strings
25
+ normalized_constraints = normalize_julia_constraint(constraint)
26
+ # Pass the array to Gem::Requirement, which accepts multiple conditions
27
+ new(normalized_constraints)
27
28
  end
28
29
  rescue Gem::Requirement::BadRequirementError
29
30
  [new(">= 0")]
@@ -41,38 +42,69 @@ module Dependabot
41
42
  version
42
43
  end
43
44
 
44
- sig { params(constraint: String).returns(String) }
45
+ sig { params(constraint: String).returns(T::Array[String]) }
45
46
  def self.normalize_julia_constraint(constraint)
46
47
  return normalize_caret_constraint(constraint) if constraint.match?(/^\^(\d+(?:\.\d+)*)/)
47
48
  return normalize_tilde_constraint(constraint) if constraint.match?(/^~(\d+(?:\.\d+)*)/)
48
49
  return normalize_range_constraint(constraint) if constraint.match?(/^(\d+(?:\.\d+)*)-(\d+(?:\.\d+)*)$/)
49
50
 
51
+ # Julia treats plain version numbers as caret constraints (implicit ^)
52
+ # e.g., "1.2.3" is equivalent to "^1.2.3" which means ">= 1.2.3, < 2.0.0"
53
+ # See: https://pkgdocs.julialang.org/v1/compatibility/
54
+ return normalize_caret_constraint("^#{constraint}") if constraint.match?(/^(\d+(?:\.\d+)*)$/)
55
+
50
56
  # Return as-is for standard gem requirements (>=, <=, ==, etc.)
51
- constraint
57
+ [constraint]
52
58
  end
53
59
 
54
- sig { params(constraint: String).returns(String) }
60
+ sig { params(constraint: String).returns(T::Array[String]) }
55
61
  private_class_method def self.normalize_caret_constraint(constraint)
56
62
  version = T.must(constraint[1..-1])
57
63
  parts = version.split(".")
58
- major = T.must(parts[0])
59
- return ">= #{version}.0.0, < #{major.to_i + 1}.0.0" if parts.length == 1
60
-
61
- ">= #{version}, < #{major.to_i + 1}.0.0"
64
+ major = T.must(parts[0]).to_i
65
+ minor = parts[1].to_i
66
+ patch = parts[2].to_i
67
+
68
+ # Julia caret semantics:
69
+ # - For 0.0.x: compatible within patch (e.g., 0.0.5 -> 0.0.x, < 0.0.6 or < 0.1.0?)
70
+ # - For 0.x.y: compatible within minor (e.g., 0.34.6 -> 0.34.x, < 0.35.0)
71
+ # - For x.y.z (x > 0): compatible within major (e.g., 1.2.3 -> 1.x.x, < 2.0.0)
72
+ if major.zero? && minor.zero?
73
+ # 0.0.x versions: bump patch
74
+ [">= #{version}", "< 0.0.#{patch + 1}"]
75
+ elsif major.zero?
76
+ # 0.x.y versions: bump minor (0.34.6 -> < 0.35.0)
77
+ [">= #{version}", "< 0.#{minor + 1}.0"]
78
+ else
79
+ # x.y.z versions where x > 0: bump major
80
+ [">= #{version}", "< #{major + 1}.0.0"]
81
+ end
62
82
  end
63
83
 
64
- sig { params(constraint: String).returns(String) }
84
+ sig { params(constraint: String).returns(T::Array[String]) }
65
85
  private_class_method def self.normalize_tilde_constraint(constraint)
66
86
  version = T.must(constraint[1..-1])
67
87
  parts = version.split(".")
68
- return ">= #{version}, < #{T.must(parts[0]).to_i + 1}.0.0" unless parts.length >= 2
69
-
70
- major = T.must(parts[0])
71
- minor = T.must(parts[1])
72
- ">= #{version}, < #{major}.#{minor.to_i + 1}.0"
88
+ major = T.must(parts[0]).to_i
89
+ minor = parts[1].to_i
90
+
91
+ # Julia tilde semantics (similar to npm):
92
+ # - For 0.0.x: compatible within patch (same as caret)
93
+ # - For 0.x.y or x.y.z: compatible within minor (bump minor)
94
+ if major.zero? && minor.zero?
95
+ # 0.0.x versions: bump patch
96
+ patch = parts[2].to_i
97
+ [">= #{version}", "< 0.0.#{patch + 1}"]
98
+ elsif major.zero?
99
+ # 0.x.y versions: bump minor (same as caret for 0.x)
100
+ [">= #{version}", "< 0.#{minor + 1}.0"]
101
+ else
102
+ # x.y.z versions where x > 0: bump minor only
103
+ [">= #{version}", "< #{major}.#{minor + 1}.0"]
104
+ end
73
105
  end
74
106
 
75
- sig { params(constraint: String).returns(String) }
107
+ sig { params(constraint: String).returns(T::Array[String]) }
76
108
  private_class_method def self.normalize_range_constraint(constraint)
77
109
  start_version, end_version = constraint.split("-")
78
110
  end_parts = T.must(end_version).split(".")
@@ -85,7 +117,7 @@ module Dependabot
85
117
  "#{T.must(end_parts[0]).to_i + 1}.0.0"
86
118
  end
87
119
 
88
- ">= #{start_version}, < #{next_minor}"
120
+ [">= #{start_version}", "< #{next_minor}"]
89
121
  end
90
122
  end
91
123
  end
@@ -53,8 +53,8 @@ module Dependabot
53
53
  def update_requirement(requirement, target_version)
54
54
  current_requirement = requirement[:requirement]
55
55
 
56
- # If requirement is "*" or nil, use target version
57
- new_requirement = if current_requirement.nil? || current_requirement == "*"
56
+ # If requirement is nil (no compat entry), use target version
57
+ new_requirement = if current_requirement.nil?
58
58
  target_version.to_s
59
59
  else
60
60
  updated_version_requirement(current_requirement, target_version)
@@ -65,24 +65,46 @@ module Dependabot
65
65
 
66
66
  sig { params(requirement_string: String, target_version: Dependabot::Julia::Version).returns(String) }
67
67
  def updated_version_requirement(requirement_string, target_version)
68
- req = Dependabot::Julia::Requirement.new(requirement_string)
69
-
70
- # If current requirement already satisfied, keep it
71
- return requirement_string if req.satisfied_by?(target_version)
72
-
73
- # Otherwise, create a new requirement that includes the target version
74
- if requirement_string.start_with?("^")
75
- # Caret requirement: ^1.2 -> update to ^new_major.new_minor if needed
76
- "^#{target_version.segments[0]}.#{target_version.segments[1] || 0}"
77
- elsif requirement_string.start_with?("~")
78
- # Tilde requirement: ~1.2.3 -> update to ~new_version
79
- "~#{target_version}"
80
- elsif requirement_string.include?("-")
81
- # Range requirement: keep as is or expand to include target
82
- requirement_string
68
+ # Don't update range requirements (e.g., "0.34-0.35") - these are explicit manual constraints
69
+ return requirement_string if requirement_string.match?(/^\d+(?:\.\d+)*-\d+(?:\.\d+)*$/)
70
+
71
+ # Parse all constraints in the requirement string
72
+ reqs = Dependabot::Julia::Requirement.requirements_array(requirement_string)
73
+
74
+ # Check if any requirement is satisfied by the target version
75
+ # Note: This uses the implicit caret semantics from the Requirement class
76
+ return requirement_string if reqs.any? { |req| req.satisfied_by?(target_version) }
77
+
78
+ # Otherwise, append a new requirement that includes the target version
79
+ # Following CompatHelper.jl's approach: use major.minor for versions >= 1.0,
80
+ # 0.minor for 0.x versions, and 0.0.patch for 0.0.x versions
81
+ new_spec = simplified_version_spec(target_version)
82
+
83
+ # Append the new spec to the existing requirement (CompatHelper KeepEntry behavior)
84
+ # Detect whether the existing requirement uses spaces after commas and preserve that format
85
+ # and default to ", " if no commas found
86
+ separator = requirement_string.include?(",") && !requirement_string.include?(", ") ? "," : ", "
87
+ "#{requirement_string}#{separator}#{new_spec}"
88
+ end
89
+
90
+ sig { params(target_version: Dependabot::Julia::Version).returns(String) }
91
+ def simplified_version_spec(target_version)
92
+ # Follow CompatHelper.jl's compat_version_number logic:
93
+ # - major > 0: use "major.minor"
94
+ # - major == 0, minor > 0: use "0.minor"
95
+ # - major == 0, minor == 0: use "0.0.patch"
96
+ # Note: CompatHelper always returns plain versions (no ^ or ~ prefix)
97
+ # Coerce segments to integers (segments may be Integer or String or nil)
98
+ major = (target_version.segments[0] || 0).to_i
99
+ minor = (target_version.segments[1] || 0).to_i
100
+ patch = (target_version.segments[2] || 0).to_i
101
+
102
+ if major.positive?
103
+ "#{major}.#{minor}"
104
+ elsif minor.positive?
105
+ "0.#{minor}"
83
106
  else
84
- # Exact version or other: use target version
85
- target_version.to_s
107
+ "0.0.#{patch}"
86
108
  end
87
109
  end
88
110
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-julia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.345.0
4
+ version: 0.347.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.345.0
18
+ version: 0.347.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: 0.345.0
25
+ version: 0.347.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -260,7 +260,7 @@ licenses:
260
260
  - MIT
261
261
  metadata:
262
262
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
263
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.345.0
263
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.347.0
264
264
  rdoc_options: []
265
265
  require_paths:
266
266
  - lib