Skip to content

Commit

Permalink
Merge pull request #2680 from fiverr/support_gradle_kotlin
Browse files Browse the repository at this point in the history
Support gradle kotlin
  • Loading branch information
brrygrdn authored Dec 14, 2020
2 parents 0f47c58 + d425a96 commit 36a8ebd
Show file tree
Hide file tree
Showing 22 changed files with 2,085 additions and 91 deletions.
44 changes: 38 additions & 6 deletions gradle/lib/dependabot/gradle/file_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@ module Gradle
class FileFetcher < Dependabot::FileFetchers::Base
require_relative "file_fetcher/settings_file_parser"

SUPPORTED_BUILD_FILE_NAMES =
%w(build.gradle build.gradle.kts).freeze

SUPPORTED_SETTINGS_FILE_NAMES =
%w(settings.gradle settings.gradle.kts).freeze

def self.required_files_in?(filenames)
filenames.include?("build.gradle")
filenames.any? do |filename|
SUPPORTED_BUILD_FILE_NAMES.include?(filename)
end
end

def self.required_files_message
"Repo must contain a build.gradle."
"Repo must contain a build.gradle / build.gradle.kts file."
end

private
Expand All @@ -27,7 +35,11 @@ def fetch_files
end

def buildfile
@buildfile ||= fetch_file_from_host("build.gradle")
@buildfile ||= begin
file = supported_build_file
@buildfile_name ||= file.name if file
fetch_file_from_host(file.name) if file
end
end

def subproject_buildfiles
Expand All @@ -39,7 +51,7 @@ def subproject_buildfiles
subproject_paths

subproject_paths.map do |path|
fetch_file_from_host(File.join(path, "build.gradle"))
fetch_file_from_host(File.join(path, @buildfile_name))
rescue Dependabot::DependencyFileNotFound
# Gradle itself doesn't worry about missing subprojects, so we don't
nil
Expand Down Expand Up @@ -74,8 +86,28 @@ def file_exists_in_submodule?(path)
end

def settings_file
@settings_file ||= fetch_file_from_host("settings.gradle")
rescue Dependabot::DependencyFileNotFound
@settings_file ||= begin
file = supported_settings_file
fetch_file_from_host(file.name) if file
rescue Dependabot::DependencyFileNotFound
nil
end
end

def supported_build_file
supported_file(SUPPORTED_BUILD_FILE_NAMES)
end

def supported_settings_file
supported_file(SUPPORTED_SETTINGS_FILE_NAMES)
end

def supported_file(supported_file_names)
supported_file_names.each do |supported_file_name|
file = fetch_file_if_present(supported_file_name)
return file if file
end

nil
end
end
Expand Down
29 changes: 20 additions & 9 deletions gradle/lib/dependabot/gradle/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class FileParser < Dependabot::FileParsers::Base
require "dependabot/file_parsers/base/dependency_set"
require_relative "file_parser/property_value_finder"

SUPPORTED_BUILD_FILE_NAMES = %w(build.gradle build.gradle.kts).freeze

PROPERTY_REGEX =
/
(?:\$\{property\((?<property_name>[^:\s]*?)\)\})|
Expand All @@ -36,6 +38,7 @@ class FileParser < Dependabot::FileParsers::Base
PLUGIN_BLOCK_DECLARATION_REGEX = /(?:^|\s)plugins\s*\{/.freeze
PLUGIN_BLOCK_ENTRY_REGEX =
/id\s+"(?<id>#{PART})"\s+version\s+"(?<version>#{VSN_PART})"/.freeze
PLUGIN_ID_REGEX = /['"](?<id>#{PART})['"]/.freeze

def parse
dependency_set = DependencySet.new
Expand All @@ -51,7 +54,7 @@ def parse
private

def map_value_regex(key)
/(?:^|\s|,|\()#{Regexp.quote(key)}:\s*['"](?<value>[^'"]+)['"]/
/(?:^|\s|,|\()#{Regexp.quote(key)}(\s*=|:)\s*['"](?<value>[^'"]+)['"]/
end

def buildfile_dependencies(buildfile)
Expand Down Expand Up @@ -146,10 +149,11 @@ def plugin_dependencies(buildfile)

plugin_blocks.each do |blk|
blk.lines.each do |line|
name = line.match(/id\s+['"](?<id>#{PART})['"]/)&.
named_captures&.fetch("id")
version = line.match(/version\s+['"](?<version>#{VSN_PART})['"]/)&.
named_captures&.fetch("version")
name_regex = /id(\s+#{PLUGIN_ID_REGEX}|\(#{PLUGIN_ID_REGEX}\))/
name = line.match(name_regex)&.named_captures&.fetch("id")
version_regex = /version\s+['"](?<version>#{VSN_PART})['"]/
version = line.match(version_regex)&.named_captures&.
fetch("version")
next unless name && version

details = { name: name, group: "plugins", version: version }
Expand Down Expand Up @@ -286,23 +290,30 @@ def closing_bracket_index(string)
end

def buildfiles
@buildfiles ||=
dependency_files.select { |f| f.name.end_with?("build.gradle") }
@buildfiles ||= dependency_files.select do |f|
f.name.end_with?(*SUPPORTED_BUILD_FILE_NAMES)
end
end

def script_plugin_files
@script_plugin_files ||=
buildfiles.flat_map do |buildfile|
buildfile.content.
scan(/apply from:\s+['"]([^'"]+)['"]/).flatten.
scan(/apply from(\s+=|:)\s+['"]([^'"]+)['"]/).flatten.
map { |f| dependency_files.find { |bf| bf.name == f } }.
compact
end.
uniq
end

def check_required_files
raise "No build.gradle!" unless get_original_file("build.gradle")
raise "No build.gradle or build.gradle.kts!" unless original_file
end

def original_file
dependency_files.find do |f|
SUPPORTED_BUILD_FILE_NAMES.include?(f.name)
end
end
end
end
Expand Down
93 changes: 86 additions & 7 deletions gradle/lib/dependabot/gradle/file_parser/property_value_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ module Gradle
class FileParser
class PropertyValueFinder
# rubocop:disable Layout/LineLength
SUPPORTED_BUILD_FILE_NAMES = %w(build.gradle build.gradle.kts).freeze

QUOTED_VALUE_REGEX =
/\s*['"][^\s]+['"]\s*/.freeze

Expand All @@ -15,20 +17,63 @@ class PropertyValueFinder
/\s*project\.findProperty\(#{QUOTED_VALUE_REGEX}\)\s*\?:/.freeze

# project.hasProperty('property') ? project.getProperty('property') :
HAS_PROPERTY_REGEX =
GROOVY_HAS_PROPERTY_REGEX =
/\s*project\.hasProperty\(#{QUOTED_VALUE_REGEX}\)\s*\?\s*project\.getProperty\(#{QUOTED_VALUE_REGEX}\)\s*:/.freeze

# if(project.hasProperty("property")) project.getProperty("property") else
KOTLIN_HAS_PROPERTY_REGEX =
/\s*if\s*\(project\.hasProperty\(#{QUOTED_VALUE_REGEX}\)\)\s+project\.getProperty\(#{QUOTED_VALUE_REGEX}\)\s+else\s+/.freeze

GROOVY_PROPERTY_DECLARATION_AS_DEFAULTS_REGEX =
/(?:#{FIND_PROPERTY_REGEX}|#{GROOVY_HAS_PROPERTY_REGEX})?/.freeze

KOTLIN_PROPERTY_DECLARATION_AS_DEFAULTS_REGEX =
/(?:#{FIND_PROPERTY_REGEX}|#{KOTLIN_HAS_PROPERTY_REGEX})?/.freeze

PROPERTY_DECLARATION_AS_DEFAULTS_REGEX =
/(?:#{FIND_PROPERTY_REGEX}|#{HAS_PROPERTY_REGEX})?/.freeze
/(#{GROOVY_PROPERTY_DECLARATION_AS_DEFAULTS_REGEX}|#{KOTLIN_PROPERTY_DECLARATION_AS_DEFAULTS_REGEX})?/.freeze

VALUE_REGEX =
/#{PROPERTY_DECLARATION_AS_DEFAULTS_REGEX}\s*['"](?<value>[^\s]+)['"]/.freeze

GROOVY_SINGLE_PROPERTY_DECLARATION_REGEX =
/(?:^|\s+|ext.)(?<name>[^\s=]+)\s*=#{VALUE_REGEX}/.freeze

KOTLIN_SINGLE_PROPERTY_INDEX_DECLARATION_REGEX =
/\s*extra\[['"](?<name>[^\s=]+)['"]\]\s*=#{VALUE_REGEX}/.freeze

KOTLIN_SINGLE_PROPERTY_SET_REGEX =
/\s*set\(['"](?<name>[^\s=]+)['"]\s*,#{VALUE_REGEX}\)/.freeze

KOTLIN_SINGLE_PROPERTY_SET_DECLARATION_REGEX =
/\s*extra\.#{KOTLIN_SINGLE_PROPERTY_SET_REGEX}/.freeze

KOTLIN_SINGLE_PROPERTY_DECLARATION_REGEX =
/(#{KOTLIN_SINGLE_PROPERTY_INDEX_DECLARATION_REGEX}|#{KOTLIN_SINGLE_PROPERTY_SET_DECLARATION_REGEX})/.freeze

SINGLE_PROPERTY_DECLARATION_REGEX =
/(?:^|\s+|ext.)(?<name>[^\s=]+)\s*=#{PROPERTY_DECLARATION_AS_DEFAULTS_REGEX}\s*['"](?<value>[^\s]+)['"]/.freeze
/(#{KOTLIN_SINGLE_PROPERTY_DECLARATION_REGEX}|#{GROOVY_SINGLE_PROPERTY_DECLARATION_REGEX})/.freeze

MULTI_PROPERTY_DECLARATION_REGEX =
GROOVY_MULTI_PROPERTY_DECLARATION_REGEX =
/(?:^|\s+|ext.)(?<namespace>[^\s=]+)\s*=\s*\[(?<values>[^\]]+)\]/m.freeze

KOTLIN_BLOCK_PROPERTY_DECLARATION_REGEX =
/\s*(?<namespace>[^\s=]+)\.apply\s*{(?<values>[^\]]+)}/m.freeze

KOTLIN_MULTI_PROPERTY_DECLARATION_REGEX =
/\s*extra\[['"](?<namespace>[^\s=]+)['"]\]\s*=\s*mapOf\((?<values>[^\]]+)\)/m.freeze

MULTI_PROPERTY_DECLARATION_REGEX =
/(#{KOTLIN_MULTI_PROPERTY_DECLARATION_REGEX}|#{GROOVY_MULTI_PROPERTY_DECLARATION_REGEX})/.freeze

KOTLIN_MAP_NAMESPACED_DECLARATION_REGEX =
/(?:^|\s+)['"](?<name>[^\s:]+)['"]\s*to#{VALUE_REGEX}\s*/.freeze

REGULAR_NAMESPACED_DECLARATION_REGEX =
/(?:^|\s+)(?<name>[^\s:]+)\s*[:=]#{VALUE_REGEX}\s*/.freeze

NAMESPACED_DECLARATION_REGEX =
/(?:^|\s+)(?<name>[^\s:]+)\s*:#{PROPERTY_DECLARATION_AS_DEFAULTS_REGEX}\s*['"](?<value>[^\s]+)['"]\s*/.freeze
/(#{REGULAR_NAMESPACED_DECLARATION_REGEX}|#{KOTLIN_MAP_NAMESPACED_DECLARATION_REGEX})/.freeze
# rubocop:enable Layout/LineLength

def initialize(dependency_files:)
Expand Down Expand Up @@ -78,6 +123,9 @@ def properties(buildfile)
@properties[buildfile.name].
merge!(fetch_single_property_declarations(buildfile))

@properties[buildfile.name].
merge!(fetch_kotlin_block_property_declarations(buildfile))

@properties[buildfile.name].
merge!(fetch_multi_property_declarations(buildfile))

Expand All @@ -104,6 +152,36 @@ def fetch_single_property_declarations(buildfile)
properties
end

def fetch_kotlin_block_property_declarations(buildfile)
properties = {}

prepared_content(buildfile).
scan(KOTLIN_BLOCK_PROPERTY_DECLARATION_REGEX) do
captures = Regexp.last_match.named_captures
namespace = captures.fetch("namespace")

captures.fetch("values").
scan(KOTLIN_SINGLE_PROPERTY_SET_REGEX) do
declaration_string = Regexp.last_match.to_s.strip
sub_captures = Regexp.last_match.named_captures
name = sub_captures.fetch("name")
full_name = if namespace == "extra"
name
else
[namespace, name].join(".")
end

properties[full_name] = {
value: sub_captures.fetch("value"),
declaration_string: declaration_string,
file: buildfile.name
}
end
end

properties
end

def fetch_multi_property_declarations(buildfile)
properties = {}

Expand Down Expand Up @@ -136,8 +214,9 @@ def prepared_content(buildfile)
end

def top_level_buildfile
@top_level_buildfile ||=
dependency_files.find { |f| f.name == "build.gradle" }
@top_level_buildfile ||= dependency_files.find do |f|
SUPPORTED_BUILD_FILE_NAMES.include?(f.name)
end
end
end
end
Expand Down
16 changes: 13 additions & 3 deletions gradle/lib/dependabot/gradle/file_parser/repositories_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ module Dependabot
module Gradle
class FileParser
class RepositoriesFinder
SUPPORTED_BUILD_FILE_NAMES = %w(build.gradle build.gradle.kts).freeze

# The Central Repo doesn't have special status for Gradle, but until
# we're confident we're selecting repos correctly it's wise to include
# it as a default.
CENTRAL_REPO_URL = "https://repo.maven.apache.org/maven2"

REPOSITORIES_BLOCK_START = /(?:^|\s)repositories\s*\{/.freeze
MAVEN_REPO_REGEX =

GROOVY_MAVEN_REPO_REGEX =
/maven\s*\{[^\}]*\surl[\s\(]\s*['"](?<url>[^'"]+)['"]/.freeze

KOTLIN_MAVEN_REPO_REGEX =
/maven\(['"](?<url>[^'"]+)['"]\)/.freeze

MAVEN_REPO_REGEX =
/(#{KOTLIN_MAVEN_REPO_REGEX}|#{GROOVY_MAVEN_REPO_REGEX})/.freeze

def initialize(dependency_files:, target_dependency_file:)
@dependency_files = dependency_files
@target_dependency_file = target_dependency_file
Expand Down Expand Up @@ -130,8 +139,9 @@ def comment_free_content(buildfile)
end

def top_level_buildfile
@top_level_buildfile ||=
dependency_files.find { |f| f.name == "build.gradle" }
@top_level_buildfile ||= dependency_files.find do |f|
SUPPORTED_BUILD_FILE_NAMES.include?(f.name)
end
end
end
end
Expand Down
15 changes: 12 additions & 3 deletions gradle/lib/dependabot/gradle/file_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ class FileUpdater < Dependabot::FileUpdaters::Base
require_relative "file_updater/dependency_set_updater"
require_relative "file_updater/property_value_updater"

SUPPORTED_BUILD_FILE_NAMES = %w(build.gradle build.gradle.kts).freeze

def self.updated_files_regex
[/^build\.gradle$/, %r{/build\.gradle$}]
[/^build\.gradle(\.kts)?$/, %r{/build\.gradle(\.kts)?$}]
end

def updated_dependency_files
Expand All @@ -38,7 +40,13 @@ def updated_dependency_files
private

def check_required_files
raise "No build.gradle!" unless get_original_file("build.gradle")
raise "No build.gradle or build.gradle.kts!" unless original_file
end

def original_file
dependency_files.find do |f|
SUPPORTED_BUILD_FILE_NAMES.include?(f.name)
end
end

def update_buildfiles_for_dependency(buildfiles:, dependency:)
Expand Down Expand Up @@ -131,7 +139,8 @@ def original_buildfile_declaration(dependency, requirement)
next false unless line.include?(dependency.name.split(":").first)
next false unless line.include?(dependency.name.split(":").last)
else
name_regex = /id\s+['"]#{Regexp.quote(dependency.name)}['"]/
name_regex_value = /['"]#{Regexp.quote(dependency.name)}['"]/
name_regex = /id(\s+#{name_regex_value}|\(#{name_regex_value}\))/
next false unless line.match?(name_regex)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@
it "includes the additional declarations" do
expect(subproject_paths).to match_array(%w(app))
end

context "when kotlin" do
let(:settings_file) do
Dependabot::DependencyFile.new(
name: "settings.gradle.kts",
content: fixture("settings_files", fixture_name)
)
end
let(:fixture_name) { "settings.gradle.kts" }

it "includes the additional declarations" do
expect(subproject_paths).to match_array(%w(app))
end
end
end

context "with commented out subproject declarations" do
Expand Down
Loading

0 comments on commit 36a8ebd

Please sign in to comment.