Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support gradle kotlin #2680

Merged
merged 3 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
shakhar marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -288,23 +292,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
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could we avoid (GROOVY|KOTLIN) within regexps?
We know from the filename which syntax to use, I'm wondering if keeping the formats split will be easier to read+maintain.

WDYT? Just an idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seem that groovy and kotlin have a lot in common, and the split can cause code duplication which IMO won't be easier to maintain.
Anyway, tell me if you want me to split it, and I'll split :)


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 @@ -82,6 +127,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 @@ -108,6 +156,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 @@ -140,8 +218,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 @@ -136,8 +145,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 @@ -133,7 +141,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