# frozen_string_literal: true # typed: strict require 'code_ownership/private/extension_loader' require 'code_ownership/private/team_plugins/ownership' require 'code_ownership/private/team_plugins/github' require 'code_ownership/private/codeowners_file' require 'code_ownership/private/parse_js_packages' require 'code_ownership/private/glob_cache' require 'code_ownership/private/validations/files_have_owners' require 'code_ownership/private/validations/github_codeowners_up_to_date' require 'code_ownership/private/validations/files_have_unique_owners' require 'code_ownership/private/ownership_mappers/file_annotations' require 'code_ownership/private/ownership_mappers/team_globs' require 'code_ownership/private/ownership_mappers/directory_ownership' require 'code_ownership/private/ownership_mappers/package_ownership' require 'code_ownership/private/ownership_mappers/js_package_ownership' require 'code_ownership/private/ownership_mappers/team_yml_ownership' module CodeOwnership module Private extend T::Sig sig { returns(Configuration) } def self.configuration @configuration ||= T.let(@configuration, T.nilable(Configuration)) @configuration ||= Configuration.fetch end # This is just an alias for `configuration` that makes it more explicit what we're doing instead of just calling `configuration`. # This is necessary because configuration may contain extensions of code ownership, so those extensions should be loaded prior to # calling APIs that provide ownership information. sig { returns(Configuration) } def self.load_configuration! configuration end sig { void } def self.bust_caches! @configuration = nil @tracked_files = nil @glob_cache = nil end sig { params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).void } def self.validate!(files:, autocorrect: true, stage_changes: true) CodeownersFile.update_cache!(files) if CodeownersFile.use_codeowners_cache? errors = Validator.all.flat_map do |validator| validator.validation_errors( files: files, autocorrect: autocorrect, stage_changes: stage_changes ) end if errors.any? errors << 'See https://github.com/rubyatscale/code_ownership#README.md for more details' raise InvalidCodeOwnershipConfigurationError.new(errors.join("\n")) # rubocop:disable Style/RaiseArgs end end # Returns a string version of the relative path to a Rails constant, # or nil if it can't find something sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(String)) } def self.path_from_klass(klass) if klass path = Object.const_source_location(klass.to_s)&.first (path && Pathname.new(path).relative_path_from(Pathname.pwd).to_s) || nil else nil end rescue NameError nil end # # The output of this function is string pathnames relative to the root. # sig { returns(T::Array[String]) } def self.tracked_files @tracked_files ||= T.let(@tracked_files, T.nilable(T::Array[String])) @tracked_files ||= Dir.glob(configuration.owned_globs) - Dir.glob(configuration.unowned_globs) end sig { params(file: String).returns(T::Boolean) } def self.file_tracked?(file) # Another way to accomplish this is # (Dir.glob(configuration.owned_globs) - Dir.glob(configuration.unowned_globs)).include?(file) # However, globbing out can take 5 or more seconds on a large repository, dramatically slowing down # invocations to `bin/codeownership validate --diff`. # Using `File.fnmatch?` is a lot faster! in_owned_globs = configuration.owned_globs.all? do |owned_glob| File.fnmatch?(owned_glob, file, File::FNM_PATHNAME | File::FNM_EXTGLOB) end in_unowned_globs = configuration.unowned_globs.any? do |unowned_glob| File.fnmatch?(unowned_glob, file, File::FNM_PATHNAME | File::FNM_EXTGLOB) end in_owned_globs && !in_unowned_globs && File.exist?(file) end sig { params(team_name: String, location_of_reference: String).returns(CodeTeams::Team) } def self.find_team!(team_name, location_of_reference) found_team = CodeTeams.find(team_name) if found_team.nil? raise StandardError, "Could not find team with name: `#{team_name}` in #{location_of_reference}. Make sure the team is one of `#{CodeTeams.all.map(&:name).sort}`" else found_team end end sig { returns(GlobCache) } def self.glob_cache @glob_cache ||= T.let(@glob_cache, T.nilable(GlobCache)) @glob_cache ||= begin if CodeownersFile.use_codeowners_cache? CodeownersFile.to_glob_cache else Mapper.to_glob_cache end end end end private_constant :Private end