Skip to content
Closed
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
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ jobs:
strategy:
matrix:
ruby:
- 3.1
- 3.2
- 3.3
- 3.4
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,4 @@ Style/HashSyntax:

Gemspec/DevelopmentDependencies:
Enabled: true
EnforcedStyle: gemspec
EnforcedStyle: Gemfile
9 changes: 9 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
source 'https://rubygems.org'

gemspec

gem 'debug'
gem 'packwerk'
gem 'railties'
gem 'rake'
gem 'rspec'
gem 'rubocop'
gem 'sorbet'
gem 'tapioca'
10 changes: 1 addition & 9 deletions code_ownership.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,5 @@ Gem::Specification.new do |spec|
spec.add_dependency 'code_teams', '~> 1.0'
spec.add_dependency 'packs-specification'
spec.add_dependency 'sorbet-runtime', '>= 0.5.11249'

spec.add_development_dependency 'debug'
spec.add_development_dependency 'packwerk'
spec.add_development_dependency 'railties'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'rubocop'
spec.add_development_dependency 'sorbet'
spec.add_development_dependency 'tapioca'
spec.add_dependency 'zeitwerk'
end
44 changes: 21 additions & 23 deletions lib/code_ownership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@

# typed: strict

require 'set'
require 'code_teams'
require 'sorbet-runtime'
require 'json'
require 'packs-specification'
require 'code_ownership/mapper'
require 'code_ownership/validator'
require 'code_ownership/private'
require 'code_ownership/cli'
require 'code_ownership/configuration'
require 'zeitwerk'

loader = Zeitwerk::Loader.for_gem
loader.setup

if defined?(Packwerk)
require 'code_ownership/private/permit_pack_owner_top_level_key'
require 'code_ownership/private/pack_ownership_validator'
end

module CodeOwnership
Expand Down Expand Up @@ -137,22 +135,22 @@ def backtrace_with_ownership(backtrace)
# ./app/controllers/some_controller.rb:43:in `block (3 levels) in create'
#
backtrace_line = if RUBY_VERSION >= '3.4.0'
%r{\A(#{Pathname.pwd}/|\./)?
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
:
(?<line>\d+) # Matches '43'
:in\s
'(?<function>.*)' # Matches "`block (3 levels) in create'"
\z}x
else
%r{\A(#{Pathname.pwd}/|\./)?
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
:
(?<line>\d+) # Matches '43'
:in\s
`(?<function>.*)' # Matches "`block (3 levels) in create'"
\z}x
end
%r{\A(#{Pathname.pwd}/|\./)?
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
:
(?<line>\d+) # Matches '43'
:in\s
'(?<function>.*)' # Matches "`block (3 levels) in create'"
\z}x
else
%r{\A(#{Pathname.pwd}/|\./)?
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
:
(?<line>\d+) # Matches '43'
:in\s
`(?<function>.*)' # Matches "`block (3 levels) in create'"
\z}x
end

backtrace.lazy.filter_map do |line|
match = line.match(backtrace_line)
Expand Down
3 changes: 2 additions & 1 deletion lib/code_ownership/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module CodeOwnership
class Configuration < T::Struct
extend T::Sig

DEFAULT_JS_PACKAGE_PATHS = T.let(['**/'], T::Array[String])

const :owned_globs, T::Array[String]
Expand Down Expand Up @@ -31,7 +32,7 @@ def self.fetch
skip_codeowners_validation: config_hash.fetch('skip_codeowners_validation', false),
raw_hash: config_hash,
require_github_teams: config_hash.fetch('require_github_teams', false),
codeowners_path: config_hash.fetch('codeowners_path', '.github'),
codeowners_path: config_hash.fetch('codeowners_path', '.github')
)
end

Expand Down
1 change: 1 addition & 0 deletions lib/code_ownership/private/extension_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Private
module ExtensionLoader
class << self
extend T::Sig

sig { params(require_directive: String).void }
def load(require_directive)
# We want to transform the require directive to behave differently
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class FileAnnotations
extend T::Sig
include Mapper

TEAM_PATTERN = T.let(%r{\A(?:#|//|-#) @team (?<team>.*)\Z}.freeze, Regexp)
TEAM_PATTERN = T.let(%r{\A(?:#|//|-#) @team (?<team>.*)\Z}, Regexp)
DESCRIPTION = 'Annotations at the top of file'

sig do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class TeamGlobs
returns(T::Hash[String, ::CodeTeams::Team])
end
def map_files_to_owners
return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count.positive?
return @@map_files_to_owners if @@map_files_to_owners&.keys&.any?

@@map_files_to_owners = CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
code_team = TeamPlugins::Ownership.for(team)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class TeamYmlOwnership
.returns(T::Hash[String, ::CodeTeams::Team])
end
def map_files_to_owners(files)
return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count.positive?
return @@map_files_to_owners if @@map_files_to_owners&.keys&.any?

@@map_files_to_owners = CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
map[team.config_yml] = team
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def validation_errors(files:, autocorrect: true, stage_changes: true)
cache = Private.glob_cache
file_mappings = cache.mapper_descriptions_that_map_files(files)
files_not_mapped_at_all = file_mappings.select do |_file, mapper_descriptions|
mapper_descriptions.count.zero?
mapper_descriptions.none?
end

errors = T.let([], T::Array[String])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ module CodeOwnership
end
end



describe '.remove_file_annotation!' do
subject(:remove_file_annotation) do
CodeOwnership.remove_file_annotation!(filename)
Expand Down
76 changes: 76 additions & 0 deletions spec/zeitwerk_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# typed: false
# frozen_string_literal: true

# Zeitwerk Compliance Smoke Test
#
# This test serves as a Zeitwerk compliance smoke test that validates the gem's
# file structure and naming conventions. It ensures that all files in the gem
# follow Zeitwerk's strict naming conventions by forcing the autoloader to
# eagerly load every constant and file in the gem.
#
# How it works:
# 1. Eager Loading: Forces Zeitwerk to immediately load all files and constants
# in the gem, rather than loading them on-demand
# 2. Error Detection: If there are any naming convention violations, Zeitwerk
# will raise an error during this process
# 3. Validation: The test passes only if no errors are raised
#
# What it catches:
# - Misnamed files (e.g., my_class.rb should define MyClass)
# - Incorrect directory structure relative to module nesting
# - Missing constants (files that exist but don't define expected constants)
# - Extra or orphaned files that don't follow naming patterns
# - Namespace violations (constants defined in wrong namespace for their location)

RSpec.describe 'zeitwerk autoloader' do
it 'werks (successfully loads the gem)' do
Zeitwerk::Loader.eager_load_namespace(CodeOwnership)
rescue StandardError => e
# Enhance the error message with more specific information
enhanced_message = build_enhanced_error_message(e)
raise enhanced_message
end

private

def build_enhanced_error_message(error)
message_parts = [
'Zeitwerk eager loading failed with the following error:',
'',
"Original Error: #{error.class}: #{error.message}",
''
]

# Add backtrace information to help identify the problematic file
if error.backtrace
gem_related_trace = error.backtrace.select do |line|
line.include?('lib/') || line.include?('zeitwerk')
end

if gem_related_trace.any?
message_parts << 'Relevant backtrace:'
gem_related_trace.first(5).each do |line|
message_parts << " #{line}"
end
message_parts << ''
end
end

# Try to identify which file might be causing the issue
if /(?:wrong constant name|uninitialized constant|expected.*to define)/i.match?(error.message)
message_parts << 'This error typically indicates:'
message_parts << "- A file name doesn't match its constant name"
message_parts << '- A constant is defined in the wrong namespace'
message_parts << "- A file exists but doesn't define the expected constant"
message_parts << ''
end

# Add helpful debugging information
message_parts << 'To debug this issue:'
message_parts << '1. Check that all files in lib/ follow zeitwerk naming conventions'
message_parts << '2. Ensure each file defines a constant matching its file path'
message_parts << '3. Verify modules/classes are in the correct namespace'

message_parts.join("\n")
end
end
Loading