Skip to content
Open
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
78 changes: 63 additions & 15 deletions bazel/lib/dependabot/bazel/file_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class FileFetcher < Dependabot::FileFetchers::Base
require_relative "file_fetcher/module_path_extractor"
require_relative "file_fetcher/directory_tree_fetcher"
require_relative "file_fetcher/downloader_config_fetcher"
require_relative "file_fetcher/include_extractor"

WORKSPACE_FILES = T.let(%w(WORKSPACE WORKSPACE.bazel).freeze, T::Array[String])
MODULE_FILE = T.let("MODULE.bazel", String)
Expand Down Expand Up @@ -126,35 +127,82 @@ def fetch_file_from_parent_directories(filename)

# Fetches files referenced in MODULE.bazel and their associated BUILD files.
# Bazel requires BUILD files to recognize directories as valid packages.
# Also fetches files included via include() statements.
sig { returns(T::Array[DependencyFile]) }
def referenced_files_from_modules
files = T.let([], T::Array[DependencyFile])
directories_with_files = T.let(Set.new, T::Set[String])
local_override_directories = T.let(Set.new, T::Set[String])

included_module_files = fetch_included_module_files(directories_with_files)
files += included_module_files
all_module_files = module_files + included_module_files

all_module_files.each do |module_file|
module_refs = fetch_module_referenced_files(module_file, directories_with_files)
files += module_refs[:files]
module_refs[:local_override_dirs].each { |dir| local_override_directories.add(dir) }
end

tree_fetcher = DirectoryTreeFetcher.new(fetcher: self)
files += tree_fetcher.fetch_build_files_for_directories(directories_with_files)
files += fetch_local_override_directory_trees(local_override_directories)

module_files.each do |module_file|
extractor = ModulePathExtractor.new(module_file: module_file)
file_paths, directory_paths = extractor.extract_paths
files
end

bzl_fetcher = BzlFileFetcher.new(module_file: module_file, fetcher: self)
bzl_files = bzl_fetcher.fetch_bzl_files
# Fetches files referenced by a single MODULE.bazel file.
sig do
params(
module_file: DependencyFile,
directories_with_files: T::Set[String]
).returns(T::Hash[Symbol, T.untyped])
end
def fetch_module_referenced_files(module_file, directories_with_files)
files = T.let([], T::Array[DependencyFile])
local_override_dirs = T.let([], T::Array[String])

bzl_files.each do |file|
dir = File.dirname(file.name)
directories_with_files.add(dir) unless dir == "."
end
extractor = ModulePathExtractor.new(module_file: module_file)
file_paths, directory_paths = extractor.extract_paths

files += bzl_files
files += fetch_paths_and_track_directories(file_paths, directories_with_files)
bzl_fetcher = BzlFileFetcher.new(module_file: module_file, fetcher: self)
bzl_files = bzl_fetcher.fetch_bzl_files

directory_paths.each { |dir| local_override_directories.add(dir) unless dir == "." }
bzl_files.each do |file|
dir = File.dirname(file.name)
directories_with_files.add(dir) unless dir == "."
end

files += tree_fetcher.fetch_build_files_for_directories(directories_with_files)
files += fetch_local_override_directory_trees(local_override_directories)
files += bzl_files
files += fetch_paths_and_track_directories(file_paths, directories_with_files)

files
directory_paths.each { |dir| local_override_dirs << dir unless dir == "." }

{ files: files, local_override_dirs: local_override_dirs }
end

# Fetches all files included via include() statements from module files.
sig { params(directories: T::Set[String]).returns(T::Array[DependencyFile]) }
def fetch_included_module_files(directories)
included_files = T.let([], T::Array[DependencyFile])
visited = T.let(Set.new, T::Set[String])

module_files.each do |module_file|
visited.add(module_file.name)
include_extractor = IncludeExtractor.new(module_file: module_file, fetcher: self)
new_files, include_dirs = include_extractor.fetch_included_files

new_files.each do |file|
unless visited.include?(file.name)
included_files << file
visited.add(file.name)
end
end

include_dirs.each { |dir| directories.add(dir) }
end

included_files
end

# Fetches files and tracks their directories for BUILD file resolution.
Expand Down
105 changes: 105 additions & 0 deletions bazel/lib/dependabot/bazel/file_fetcher/include_extractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# typed: strict
# frozen_string_literal: true

require "dependabot/bazel/file_fetcher"
require "dependabot/bazel/file_fetcher/path_converter"
require "sorbet-runtime"

module Dependabot
module Bazel
class FileFetcher < Dependabot::FileFetchers::Base
# Extracts include() statements from MODULE.bazel files and fetches the included files.
# Bazel's include() directive allows splitting MODULE.bazel content across multiple files.
# The include() statement uses Bazel label syntax: include("//path:file.MODULE.bazel")
# See https://bazel.build/rules/lib/globals/module#include
class IncludeExtractor
extend T::Sig

sig do
params(
module_file: DependencyFile,
fetcher: FileFetcher
).void
end
def initialize(module_file:, fetcher:)
@module_file = module_file
@fetcher = fetcher
@visited_files = T.let(Set.new, T::Set[String])
end

# Fetches all files included via include() statements, recursively.
sig { returns([T::Array[DependencyFile], T::Set[String]]) }
def fetch_included_files
files = T.let([], T::Array[DependencyFile])
directories = T.let(Set.new, T::Set[String])

content = T.must(@module_file.content)
include_paths = extract_include_paths(content)

include_paths.each do |path|
next if @visited_files.include?(path)

@visited_files.add(path)

fetched_file = @fetcher.send(:fetch_file_if_present, path)
next unless fetched_file

files << fetched_file

dir = File.dirname(path)
directories.add(dir) unless dir == "."

nested_files, nested_dirs = fetch_nested_includes(fetched_file)
files.concat(nested_files)
nested_dirs.each { |d| directories.add(d) }
end

[files, directories]
end

private

sig { returns(DependencyFile) }
attr_reader :module_file

sig { returns(FileFetcher) }
attr_reader :fetcher

sig { returns(T::Set[String]) }
attr_reader :visited_files

# Extracts file paths from include() statements.
# Only extracts workspace-relative paths (//...) and filters out external repositories.
sig { params(content: String).returns(T::Array[String]) }
def extract_include_paths(content)
paths = []

# Match include("//path:file") and include("//path/to:file.MODULE.bazel")
content.scan(%r{include\s*\(\s*"(//[^"]+)"}) do |match|
label = match[0]
path = PathConverter.label_to_path(label)
paths << path unless path.empty?
end

# Match include(":file") for same-directory includes
content.scan(/include\s*\(\s*"(:[^"]+)"/) do |match|
label = match[0]
context_dir = File.dirname(@module_file.name)
context_dir = nil if context_dir == "."
path = PathConverter.label_to_path(label, context_dir: context_dir)
paths << path unless path.empty?
end

paths.uniq
end

sig { params(included_file: DependencyFile).returns([T::Array[DependencyFile], T::Set[String]]) }
def fetch_nested_includes(included_file)
nested_extractor = IncludeExtractor.new(module_file: included_file, fetcher: @fetcher)
nested_extractor.instance_variable_set(:@visited_files, @visited_files)
nested_extractor.fetch_included_files
end
end
end
end
end
Loading
Loading