Skip to content
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
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Sorbet/StrictSigil:
Exclude:
- tasks/cop_documentation.rake
- spec/**/*
# Loaded by the gemspec before sorbet-runtime, so `T` is unavailable here.
- lib/rubocop/packs/version.rb

Metrics/ParameterLists:
Enabled: false
Expand Down
16 changes: 16 additions & 0 deletions .simplecov
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# typed: false
# frozen_string_literal: true

SimpleCov.start do
enable_coverage :branch

# Track only the cops (and their direct helpers); the gate enforces that the
# cop surface is fully exercised.
add_filter { |src| !src.filename.include?('/lib/rubocop/cop/') }

# Enforce full coverage on complete runs (and when COVERAGE=true), but not
# when running a single spec file locally.
if ENV['COVERAGE'] == 'true' || ARGV.none? { |arg| arg.end_with?('_spec.rb') }
minimum_coverage line: 100, branch: 100
end
end
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ GEM
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
diff-lcs (1.6.2)
docile (1.4.1)
drb (2.2.3)
erubi (1.13.1)
i18n (1.14.8)
Expand Down Expand Up @@ -133,6 +134,12 @@ GEM
rubocop (>= 1.75.2)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sorbet (0.6.12992)
sorbet-static (= 0.6.12992)
sorbet-runtime (0.6.12992)
Expand Down Expand Up @@ -185,6 +192,7 @@ DEPENDENCIES
rubocop-extension-generator
rubocop-gusto
rubocop-packs!
simplecov
sorbet
tapioca

Expand Down
12 changes: 12 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Packs/ClassMethodsAsPublicApis:
Description: 'Requires public API methods (in `app/public`) to be defined as class methods, which are more easily statically analyzable and typically hold less state.'
Enabled: false
VersionAdded: '0.0.2'
AcceptableParentClasses:
- T::Enum
- T::Struct
Expand All @@ -8,18 +10,28 @@ Packs/ClassMethodsAsPublicApis:
AcceptableMixins: []

Packs/RootNamespaceIsPackName:
Description: 'Requires that each file is namespaced under its pack name, so that each pack exposes exactly one root namespace.'
Enabled: false
VersionAdded: '0.0.2'

Packs/TypedPublicApis:
Description: "Requires that each pack's public API (in `app/public`) is `typed: strict`."
Enabled: false
VersionAdded: '0.0.2'

Packs/DocumentedPublicApis:
Description: 'Requires that each public API method (in `app/public`) has a documentation comment.'
Enabled: false
VersionAdded: '0.0.2'

PackwerkLite/Privacy:
Description: 'Detects references to the private implementation of another pack instead of its public API.'
# It is recommended to use packwerk
Enabled: false
VersionAdded: '0.0.12'

PackwerkLite/Dependency:
Description: 'Detects references to a pack that is not listed as an explicit dependency.'
# It is recommended to use packwerk
Enabled: false
VersionAdded: '0.0.12'
5 changes: 0 additions & 5 deletions lib/rubocop/cop/packs/class_methods_as_public_apis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ module Packs
class ClassMethodsAsPublicApis < Base
extend T::Sig

sig { returns(T::Boolean) }
def support_autocorrect?
false
end

sig { params(node: T.untyped).void }
def on_def(node)
# This cop only applies for ruby files in `app/public`
Expand Down
5 changes: 0 additions & 5 deletions lib/rubocop/cop/packs/documented_public_apis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ class DocumentedPublicApis < Style::DocumentationMethod
# support this for some packs.
extend T::Sig

sig { returns(T::Boolean) }
def support_autocorrect?
false
end

sig { params(node: T.untyped).void }
def check(node)
# This cop only applies for ruby files in `app/public`
Expand Down
11 changes: 5 additions & 6 deletions lib/rubocop/cop/packs/root_namespace_is_pack_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ def on_new_investigation
return if package_for_path.nil?

namespace_context = desired_zeitwerk_api.for_file(relative_filename, package_for_path)
# :nocov: defensive: for_file always returns a context for the `app/` paths that reach here
return if namespace_context.nil?
# :nocov:

allowed_global_namespaces = Set.new(
[
Expand Down Expand Up @@ -75,12 +77,9 @@ def on_new_investigation
end
end

# In the future, we'd love this to support auto-correct.
# Perhaps by automatically renamespacing the file and changing its location?
sig { returns(T::Boolean) }
def support_autocorrect?
false
end
# In the future, we'd love this to support auto-correct,
# perhaps by automatically renamespacing the file and changing its location.
# That would mean extending `AutoCorrector` and implementing the correction.

private

Expand Down
2 changes: 2 additions & 0 deletions lib/rubocop/cop/packwerk_lite/constant_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ def self.resolve(node, processed_source)
#
# Therefore when we've found possible files, we can sanity check there is only one,
# and then assume the found pack defines the constant!
# :nocov: defensive invariant: Zeitwerk guarantees a constant maps to at most one file
raise if found_files.count > 1
# :nocov:

expected_pack_contains_constant = found_files.any?

Expand Down
5 changes: 0 additions & 5 deletions lib/rubocop/cop/packwerk_lite/dependency_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ module PackwerkLite
class Dependency < Base
extend T::Sig

sig { returns(T::Boolean) }
def support_autocorrect?
false
end

sig { params(node: RuboCop::AST::ConstNode).void }
def on_const(node)
return if Private.partial_const_reference?(node)
Expand Down
5 changes: 0 additions & 5 deletions lib/rubocop/cop/packwerk_lite/privacy_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ module PackwerkLite
class Privacy < Base
extend T::Sig

sig { returns(T::Boolean) }
def support_autocorrect?
false
end

sig { params(node: RuboCop::AST::ConstNode).void }
def on_const(node)
# See https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cop/lint/constant_resolution.rb source code as an example
Expand Down
7 changes: 6 additions & 1 deletion lib/rubocop/packs/plugin.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: strict
# frozen_string_literal: true

require 'lint_roller'
Expand All @@ -7,6 +7,9 @@ module RuboCop
module Packs
# A plugin that integrates rubocop-packs with RuboCop's plugin system.
class Plugin < LintRoller::Plugin
extend T::Sig

sig { returns(LintRoller::About) }
def about
LintRoller::About.new(
name: 'rubocop-packs',
Expand All @@ -16,10 +19,12 @@ def about
)
end

sig { params(context: LintRoller::Context).returns(T::Boolean) }
def supported?(context)
context.engine == :rubocop
end

sig { params(_context: LintRoller::Context).returns(LintRoller::Rules) }
def rules(_context)
LintRoller::Rules.new(
type: :path,
Expand Down
8 changes: 4 additions & 4 deletions manual/cops_packs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Disabled | Yes | No | - | -
Disabled | Yes | No | 0.0.2 | -

This cop states that public API should live on class methods, which are more easily statically analyzable,
searchable, and typically hold less state.
Expand Down Expand Up @@ -43,7 +43,7 @@ AcceptableMixins | `[]` | Array

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Disabled | Yes | No | - | -
Disabled | Yes | No | 0.0.2 | -

This cop helps ensure that each pack has a documented public API
The following examples assume this basic setup.
Expand Down Expand Up @@ -78,7 +78,7 @@ end

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Disabled | Yes | No | - | -
Disabled | Yes | No | 0.0.2 | -

This cop helps ensure that each pack exposes one namespace.
Note that this cop doesn't necessarily expect you to be using packs-rails (https://github.com/rubyatscale/packs-rails),
Expand All @@ -102,7 +102,7 @@ class Foo::Blah::Bar; end

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Disabled | Yes | Yes | - | -
Disabled | Yes | Yes | 0.0.2 | -

This cop helps ensure that each pack's public API is strictly typed, enforcing strong boundaries.

Expand Down
6 changes: 3 additions & 3 deletions manual/cops_packwerklite.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Disabled | Yes | No | - | -
Disabled | Yes | No | 0.0.12 | -

This cop helps ensure that packs are depending on packs explicitly.

Expand Down Expand Up @@ -45,9 +45,9 @@ end

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Disabled | Yes | No | - | -
Disabled | Yes | No | 0.0.12 | -

This cop helps ensure that packs are using public API of other systems
This cop helps ensure that packs are using the public API of other systems
The following examples assume this basic setup.

### Examples
Expand Down
1 change: 1 addition & 0 deletions rubocop-packs.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'rubocop-extension-generator'
spec.add_development_dependency 'rubocop-gusto'
spec.add_development_dependency 'simplecov'
spec.add_development_dependency 'sorbet'
spec.add_development_dependency 'tapioca'
end
57 changes: 57 additions & 0 deletions spec/rubocop/cop/packs/class_methods_as_public_apis_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,63 @@
write_file('packs/tool/app/public/tool.rb')
end

context 'when an instance method is defined at the top level with no class or module' do
let(:source) do
<<~RUBY
def my_instance_method
^^^^^^^^^^^^^^^^^^^^^^ Public API method must be a class method (e.g. `self.my_instance_method(...)`)
end
RUBY
end

it { expect_offense source, Pathname.pwd.join('packs/tool/app/public/tool.rb').to_s }
end

context 'when a class includes a dynamic (non-constant) mixin' do
let(:source) do
<<~RUBY
class Tool
include some_dynamic_mixin
def my_instance_method
^^^^^^^^^^^^^^^^^^^^^^ Public API method must be a class method (e.g. `self.my_instance_method(...)`)
end
end
RUBY
end

it { expect_offense source, Pathname.pwd.join('packs/tool/app/public/tool.rb').to_s }
end

context 'when a class includes a non-acceptable mixin' do
let(:source) do
<<~RUBY
class Tool
include SomeOtherMixin
def my_instance_method
^^^^^^^^^^^^^^^^^^^^^^ Public API method must be a class method (e.g. `self.my_instance_method(...)`)
end
end
RUBY
end

it { expect_offense source, Pathname.pwd.join('packs/tool/app/public/tool.rb').to_s }
end

context 'when a class includes more than one constant in a single include' do
let(:source) do
<<~RUBY
class Tool
include First, Second
def my_instance_method
^^^^^^^^^^^^^^^^^^^^^^ Public API method must be a class method (e.g. `self.my_instance_method(...)`)
end
end
RUBY
end

it { expect_offense source, Pathname.pwd.join('packs/tool/app/public/tool.rb').to_s }
end

context 'when class defines an instance method, does not inherit from anything' do
let(:source) do
<<~RUBY
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# typed: false
# frozen_string_literal: true

# Unit specs for the private DesiredZeitwerkApi helper. The cop only invokes it
# for `app/` paths, but the helper also supports `lib/` for API generality, so
# we exercise it directly here (imagine a not-yet-zeitwerk-compliant codebase).
RSpec.describe RuboCop::Cop::Packs::RootNamespaceIsPackName.const_get(:DesiredZeitwerkApi) do # rubocop:disable Sorbet/ConstantsFromStrings
subject(:api) { described_class.new }

before { write_pack('packs/apples') }

let(:pack) { Packs.find('packs/apples') }

context 'when the file lives under app/' do
it 'computes an app-based namespace context' do
context = api.for_file('packs/apples/app/services/tool.rb', pack)

expect(context.expected_namespace).to eq('Apples')
expect(context.expected_filepath).to eq('packs/apples/app/services/apples/tool.rb')
end
end

context 'when the file lives under lib/' do
it 'computes a lib-based namespace context' do
context = api.for_file('packs/apples/lib/tool.rb', pack)

expect(context.expected_filepath).to include('packs/apples/lib/')
end
end

context 'when the file is in neither app/ nor lib/' do
it 'raises because the autoload folder cannot be determined' do
expect { api.for_file('packs/apples/tool.rb', pack) }.to raise_error(TypeError)
end
end
end
Loading
Loading