From 10d9cb51a99b2638ae0f2d6fde862e84dc11a798 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 08:17:02 +0000 Subject: [PATCH] Parse dependency lines positionally to fix keyword artifact ids A dependency:list line is groupId:artifactId:type[:classifier]:version:scope:/path/to/file Fields were located by matching a keyword against the whole line, which picks the wrong colon when the artifact id itself equals a scope/type keyword (runtime, compile, test, provided, system, jar, pom). For com.dylibso.chicory:runtime the gav became com.dylibso.chicory:jar:1.7.5, producing a broken require_jar line (LoadError at runtime); the scope and system flag were likewise misdetected. Peel off the trailing file path first (it may contain a ':' on Windows, C:\...) via a single match, then split the remaining colon-clean coordinate once and address every field by position: type is field 3, version is last, an extra field between them is the classifier, scope is last. gav and coord are rebuilt from those parts. This removes the keyword regexes entirely and also handles an artifact id equal to the type itself. Co-Authored-By: Claude Opus 4.8 --- lib/jars/installer.rb | 50 ++++++++++++++++++++++------------------ specs/dependency_spec.rb | 21 +++++++++++++++++ 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/lib/jars/installer.rb b/lib/jars/installer.rb index 213f628..acc3988 100644 --- a/lib/jars/installer.rb +++ b/lib/jars/installer.rb @@ -6,9 +6,13 @@ module Jars class Installer class Dependency - REG = /:jar:|:pom:|:test:|:compile:|:runtime:|:provided:|:system:/.freeze EMPTY = "" + # the trailing ":" of a dependency line; the file may itself contain + # a ':' on Windows (C:\...), hence the optional drive-letter prefix + TRAILING_FILE = /:(?(?:[A-Z]:\\)?[^:]+)\z/.freeze + # A dependency:list line has the form + # groupId:artifactId:type[:classifier]:version:scope:/path/to/file def self.parse(line) return unless /:jar:|:pom:/.match?(line) @@ -18,47 +22,49 @@ def self.parse(line) line.gsub!(/ -- module.*/, EMPTY) line.strip! - if line.index(':pom:') - type = :pom - elsif line.index(':jar:') - type = :jar + # Peel off the trailing file path first: since it may contain a ':' it + # cannot take part in the split below. What is left (pre_match) is + # colon-clean, so every field is addressed by position -- never by + # matching a keyword, which breaks when an artifact id is itself a scope + # or type keyword (e.g. "com.dylibso.chicory:runtime:jar:1.7.5"). + tail = line.match(TRAILING_FILE) + file = tail[:file] + + # classifier is the only optional field, so there are exactly 5 or 6 + components = tail.pre_match.split(':') + if components.size == 6 + group_id, artifact_id, type, classifier, version, scope_name = components + else + group_id, artifact_id, type, version, scope_name = components end - coord = line.sub(/:[^:]+:([A-Z]:\\)?[^:]+$/, EMPTY) - first, second = coord.split(/:#{type}:/) - group_id, artifact_id = first.split(':') - parts = group_id.split('.') - parts << artifact_id - parts << second.split(':')[-1] - file = line.slice(coord.length, line.length).sub(REG, EMPTY).strip - last = file.reverse.index(%r{\\|/}) - parts << line[-last..] - path = File.join(parts).strip + coord = [group_id, artifact_id, type, classifier, version].compact.join(':') + gav = [group_id, artifact_id, classifier, version].compact.join(':') + path = File.join(*group_id.split('.'), artifact_id, version, file[%r{[^\\/]+\z}]) scope = - case line - when /:provided:/ + case scope_name + when 'provided' :provided - when /:test:/ + when 'test' :test else :runtime end - new(type, scope, coord, file, path, !line.index(':system:').nil?) + new(type.to_sym, scope, coord, gav, file, path, scope_name == 'system') end attr_reader :path, :file, :gav, :scope, :type, :coord - def initialize(type, scope, coord, file, path, system) + def initialize(type, scope, coord, gav, file, path, system) @type = type @scope = scope @coord = coord + @gav = gav @file = file @path = path - @system = system - @gav = @coord.sub(REG, ':') end def system? diff --git a/specs/dependency_spec.rb b/specs/dependency_spec.rb index 32d901d..0f01d8f 100644 --- a/specs/dependency_spec.rb +++ b/specs/dependency_spec.rb @@ -60,6 +60,27 @@ _(dep.file).must_equal '/usr/local/repository/org/apache/maven/maven-repository-metadata/3.1.0/maven-repository-metadata-3.1.0.pom' end + it 'should parse dependency where artifact_id is a scope keyword' do + %w[runtime compile test provided system].each do |keyword| + dep = Jars::Installer::Dependency.parse(" com.dylibso.chicory:#{keyword}:jar:1.7.5:compile:/usr/local/repository/com/dylibso/chicory/#{keyword}/1.7.5/#{keyword}-1.7.5.jar") + _(dep.type).must_equal :jar + _(dep.scope).must_equal :runtime + _(dep.system?).must_equal false + _(dep.gav).must_equal "com.dylibso.chicory:#{keyword}:1.7.5" + _(dep.coord).must_equal "com.dylibso.chicory:#{keyword}:jar:1.7.5" + _(dep.path).must_equal "com/dylibso/chicory/#{keyword}/1.7.5/#{keyword}-1.7.5.jar" + _(dep.file).must_equal "/usr/local/repository/com/dylibso/chicory/#{keyword}/1.7.5/#{keyword}-1.7.5.jar" + end + end + + it 'should generate the correct require_jar line when artifact_id is a scope keyword' do + require 'stringio' + dep = Jars::Installer::Dependency.parse(' com.dylibso.chicory:runtime:jar:1.7.5:compile:/usr/local/repository/com/dylibso/chicory/runtime/1.7.5/runtime-1.7.5.jar') + io = StringIO.new + Jars::Installer.print_require_jar(io, dep) + _(io.string.strip).must_equal "require_jar 'com.dylibso.chicory', 'runtime', '1.7.5'" + end + it 'should parse dependency where artifact_id has dots' do dep = Jars::Installer::Dependency.parse(+' org.eclipse.sisu:org.eclipse.sisu.plexus:jar:0.0.0.M2a:compile:/usr/local/repository/org/eclipse/sisu/org.eclipse.sisu.plexus/0.0.0.M2a/org.eclipse.sisu.plexus-0.0.0.M2a.jar') _(dep.type).must_equal :jar