query_packwerk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +90 -0
- data/CHANGELOG.md +1 -0
- data/CODE_OF_CONDUCT.md +131 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/exe/query_packwerk +7 -0
- data/lib/query_packwerk/cli.rb +19 -0
- data/lib/query_packwerk/console.rb +65 -0
- data/lib/query_packwerk/console_helpers.rb +144 -0
- data/lib/query_packwerk/file_cache.rb +160 -0
- data/lib/query_packwerk/package.rb +129 -0
- data/lib/query_packwerk/packages.rb +78 -0
- data/lib/query_packwerk/query_interface.rb +268 -0
- data/lib/query_packwerk/rule_rewriter/anonymize_arguments_rule.rb +31 -0
- data/lib/query_packwerk/rule_rewriter/anonymize_keyword_arguments_rule.rb +30 -0
- data/lib/query_packwerk/rule_rewriter/base_rule.rb +30 -0
- data/lib/query_packwerk/rule_rewriter/rule_set_rewriter.rb +56 -0
- data/lib/query_packwerk/rule_rewriter.rb +22 -0
- data/lib/query_packwerk/version.rb +6 -0
- data/lib/query_packwerk/violation.rb +295 -0
- data/lib/query_packwerk/violations.rb +270 -0
- data/lib/query_packwerk.rb +92 -0
- data/sig/query_packwerk.rbs +4 -0
- metadata +153 -0
@@ -0,0 +1,160 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module QueryPackwerk
|
5
|
+
# Manages caching of file contents, AST nodes, and code analysis results.
|
6
|
+
# Provides efficient access to parsed Ruby code, constant references, and
|
7
|
+
# source abstractions for performance optimization during violation analysis.
|
8
|
+
# Supports both standard and anonymized views of source code patterns.
|
9
|
+
class FileCache
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
RUBY_FILE = T.let(/\.(rb|rake)\z/, Regexp)
|
13
|
+
|
14
|
+
sig { void }
|
15
|
+
def initialize
|
16
|
+
# { file_name => AST }
|
17
|
+
@file_ast_cache = T.let({}, T::Hash[String, RuboCop::AST::Node])
|
18
|
+
@file_cache = T.let({}, T::Hash[String, String])
|
19
|
+
|
20
|
+
# { [file_name, const_name] => [AST const nodes] }
|
21
|
+
@file_const_cache = T.let({}, T::Hash[T::Array[String], T::Array[RuboCop::AST::Node]])
|
22
|
+
|
23
|
+
# { [file_name, const_name] => [AST const nodes with full receiver chains] }
|
24
|
+
@full_source_cache = T.let({}, T::Hash[T::Array[String], T::Array[RuboCop::AST::Node]])
|
25
|
+
|
26
|
+
# { [file_name, const_name] => [anonymized sources] }
|
27
|
+
@anonymized_source_cache = T.let({}, T::Hash[T::Array[String], T::Array[String]])
|
28
|
+
|
29
|
+
@anonymized_args_cache = T.let({}, T::Hash[String, String])
|
30
|
+
|
31
|
+
@is_active_record_cache = T.let({}, T::Hash[String, String])
|
32
|
+
@is_constant_cache = T.let({}, T::Hash[String, String])
|
33
|
+
end
|
34
|
+
|
35
|
+
sig { params(file_names: String, headers: T::Boolean).void }
|
36
|
+
def load!(*file_names, headers: true)
|
37
|
+
file_count = file_names.size
|
38
|
+
|
39
|
+
warn "Prepopulating AST cache with #{file_count} files: " if headers
|
40
|
+
|
41
|
+
file_names.each do |f|
|
42
|
+
get_file_ast(f)
|
43
|
+
$stderr.print '.'
|
44
|
+
end
|
45
|
+
|
46
|
+
warn '', 'AST cache loaded' if headers
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(cache_name: Symbol, key: T.untyped, value: T.untyped).returns(T.untyped) }
|
50
|
+
def set(cache_name, key:, value:)
|
51
|
+
case cache_name
|
52
|
+
when :is_active_record
|
53
|
+
return @is_active_record_cache[key] if @is_active_record_cache.key?(key)
|
54
|
+
|
55
|
+
@is_active_record_cache[key] = value
|
56
|
+
when :is_constant
|
57
|
+
return @is_constant_cache[key] if @is_constant_cache.key?(key)
|
58
|
+
|
59
|
+
@is_constant_cache[key] = value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { params(file_name: String).returns(RuboCop::AST::Node) }
|
64
|
+
def get_file_ast(file_name)
|
65
|
+
@file_ast_cache[file_name] ||= ast_from(get_file(file_name))
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(file_name: String).returns(String) }
|
69
|
+
def get_file(file_name)
|
70
|
+
@file_cache[file_name] ||= if RUBY_FILE.match?(file_name) && File.exist?(file_name)
|
71
|
+
File.read(file_name)
|
72
|
+
else
|
73
|
+
'x'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get all occurrencs of the violation's constant in a file
|
78
|
+
sig { params(file_name: String, class_name: String).returns(T::Array[RuboCop::AST::Node]) }
|
79
|
+
def get_all_const_occurrences(file_name:, class_name:)
|
80
|
+
const_key = [file_name, class_name]
|
81
|
+
|
82
|
+
return T.must(@file_const_cache[const_key]) if @file_const_cache.key?(const_key)
|
83
|
+
|
84
|
+
absolute_const_node = ast_from(class_name)
|
85
|
+
relative_const_node = ast_from(class_name.delete_prefix('::'))
|
86
|
+
|
87
|
+
@file_const_cache[const_key] = get_file_ast(file_name).each_descendant.select do |node|
|
88
|
+
next false unless node.const_type?
|
89
|
+
|
90
|
+
node == absolute_const_node || node == relative_const_node
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Gets the full unanonymized source of how a constant is called
|
95
|
+
sig { params(file_name: String, class_name: String).returns(T::Array[RuboCop::AST::Node]) }
|
96
|
+
def get_full_sources(file_name:, class_name:)
|
97
|
+
const_key = [file_name, class_name]
|
98
|
+
|
99
|
+
return T.must(@full_source_cache[const_key]) if @full_source_cache.key?(const_key)
|
100
|
+
|
101
|
+
@full_source_cache[const_key] = get_all_const_occurrences(
|
102
|
+
file_name: file_name,
|
103
|
+
class_name: class_name
|
104
|
+
).map do |node|
|
105
|
+
get_full_receiver_chain(node)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Cleans up and anonymizes a source
|
110
|
+
sig { params(source: String).returns(String) }
|
111
|
+
def anonymize_arguments(source)
|
112
|
+
@anonymized_args_cache[source] ||= RuleRewriter
|
113
|
+
.rewrite(source)
|
114
|
+
.delete("\n").squeeze(' ').delete_prefix('::')
|
115
|
+
end
|
116
|
+
|
117
|
+
# Get the full receiver chains on a constant, and anonymize their arguments
|
118
|
+
sig { params(file_name: String, class_name: String).returns(T::Array[String]) }
|
119
|
+
def get_full_anonymous_sources(file_name:, class_name:)
|
120
|
+
const_key = [file_name, class_name]
|
121
|
+
|
122
|
+
return T.must(@anonymized_source_cache[const_key]) if @anonymized_source_cache.key?(const_key)
|
123
|
+
|
124
|
+
@anonymized_source_cache[const_key] = get_full_sources(
|
125
|
+
file_name: file_name,
|
126
|
+
class_name: class_name
|
127
|
+
).map do |node|
|
128
|
+
get_full_receiver_chain(node)
|
129
|
+
.source
|
130
|
+
.then { |s| anonymize_arguments(s) }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# Turns a string into a Ruby AST
|
137
|
+
sig { params(string: String).returns(RuboCop::AST::Node) }
|
138
|
+
def ast_from(string)
|
139
|
+
RuboCop::ProcessedSource.new(string, RUBY_VERSION.to_f).ast
|
140
|
+
end
|
141
|
+
|
142
|
+
# We can find a constant, but by going up its parents we can find out the full call chain
|
143
|
+
# by checking if each parent is a receiver of the child, giving us method calls and
|
144
|
+
# arguments on a constant as well as where it occurred.
|
145
|
+
sig { params(node: RuboCop::AST::Node).returns(RuboCop::AST::Node) }
|
146
|
+
def get_full_receiver_chain(node)
|
147
|
+
return node unless node.const_type?
|
148
|
+
|
149
|
+
current_node = T.let(node, RuboCop::AST::Node)
|
150
|
+
|
151
|
+
while (parent_node = current_node.parent)
|
152
|
+
break unless parent_node.receiver == current_node
|
153
|
+
|
154
|
+
current_node = parent_node
|
155
|
+
end
|
156
|
+
|
157
|
+
current_node
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module QueryPackwerk
|
5
|
+
# Represents a Packwerk package with enhanced querying capabilities.
|
6
|
+
# Wraps around ParsePackwerk::Package to provide additional methods
|
7
|
+
# for accessing package properties, dependencies, violations, and consumer information.
|
8
|
+
class Package
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { returns(ParsePackwerk::Package) }
|
12
|
+
attr_reader :original_package
|
13
|
+
|
14
|
+
sig { params(original_package: ParsePackwerk::Package).void }
|
15
|
+
def initialize(original_package:)
|
16
|
+
@original_package = original_package
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { returns(String) }
|
20
|
+
def name
|
21
|
+
@original_package.name
|
22
|
+
end
|
23
|
+
|
24
|
+
sig { returns(T::Boolean) }
|
25
|
+
def enforce_dependencies
|
26
|
+
!!@original_package.enforce_dependencies
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { returns(T::Boolean) }
|
30
|
+
def enforce_privacy
|
31
|
+
!!@original_package.enforce_privacy
|
32
|
+
end
|
33
|
+
|
34
|
+
sig { returns(ParsePackwerk::MetadataYmlType) }
|
35
|
+
def metadata
|
36
|
+
@original_package.metadata
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
40
|
+
def config
|
41
|
+
@original_package.config
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { returns(QueryPackwerk::Packages) }
|
45
|
+
def dependencies
|
46
|
+
Packages.where(name: @original_package.dependencies)
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { returns(T::Array[String]) }
|
50
|
+
def dependency_names
|
51
|
+
@original_package.dependencies
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { returns(String) }
|
55
|
+
def owner
|
56
|
+
config['owner'] || Packages::UNOWNED
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { returns(Pathname) }
|
60
|
+
def directory
|
61
|
+
Pathname.new(name).cleanpath
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns violations where this package is the consumer (i.e., this package
|
65
|
+
# has dependencies on other packages). These are the "todos" in the package_todo.yml
|
66
|
+
# file for this package.
|
67
|
+
sig { returns(QueryPackwerk::Violations) }
|
68
|
+
def todos
|
69
|
+
QueryPackwerk::Violations.where(consuming_pack: name)
|
70
|
+
end
|
71
|
+
alias outgoing_violations todos
|
72
|
+
|
73
|
+
# Returns violations where this package is the producer (i.e., other packages
|
74
|
+
# depend on this package). These are violations where other packages are
|
75
|
+
# accessing code from this package.
|
76
|
+
sig { returns(QueryPackwerk::Violations) }
|
77
|
+
def violations
|
78
|
+
QueryPackwerk::Violations.where(producing_pack: name)
|
79
|
+
end
|
80
|
+
alias incoming_violations violations
|
81
|
+
|
82
|
+
# Returns all packages that consume (depend on) this package
|
83
|
+
sig { returns(QueryPackwerk::Packages) }
|
84
|
+
def consumers
|
85
|
+
Packages.where(name: consumer_names)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the names of all packages that consume (depend on) this package
|
89
|
+
sig { returns(T::Array[String]) }
|
90
|
+
def consumer_names
|
91
|
+
violations.consumers.keys
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns a count of how often each consumer package accesses this package
|
95
|
+
sig { returns(T::Hash[String, Integer]) }
|
96
|
+
def consumer_counts
|
97
|
+
violations.consumers
|
98
|
+
end
|
99
|
+
|
100
|
+
sig { returns(String) }
|
101
|
+
def parent_name
|
102
|
+
directory.dirname.to_s
|
103
|
+
end
|
104
|
+
|
105
|
+
sig do
|
106
|
+
params(
|
107
|
+
keys: T.nilable(T::Array[Symbol])
|
108
|
+
).returns(T::Hash[Symbol, T.untyped])
|
109
|
+
end
|
110
|
+
def deconstruct_keys(keys)
|
111
|
+
all_values = {
|
112
|
+
name: name,
|
113
|
+
owner: owner,
|
114
|
+
owned: owner != Packages::UNOWNED,
|
115
|
+
dependencies: dependency_names,
|
116
|
+
|
117
|
+
# Used for future implementations of NestedPacks
|
118
|
+
parent_name: parent_name
|
119
|
+
}
|
120
|
+
|
121
|
+
keys.nil? ? all_values : all_values.slice(*T.unsafe(keys))
|
122
|
+
end
|
123
|
+
|
124
|
+
sig { returns(String) }
|
125
|
+
def inspect
|
126
|
+
"#<#{self.class.name} #{name}>"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module QueryPackwerk
|
5
|
+
# A collection class for managing and querying sets of Packwerk packages.
|
6
|
+
# Provides methods for retrieving, filtering, and analyzing packages within
|
7
|
+
# the application. Implements Enumerable and QueryInterface for flexible
|
8
|
+
# data manipulation and consistent query patterns.
|
9
|
+
class Packages
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Generic
|
12
|
+
|
13
|
+
Elem = type_member { { fixed: QueryPackwerk::Package } }
|
14
|
+
|
15
|
+
UNOWNED = T.let('Unowned', String)
|
16
|
+
|
17
|
+
include Enumerable
|
18
|
+
include QueryInterface
|
19
|
+
|
20
|
+
sig { override.returns(T::Array[QueryPackwerk::Package]) }
|
21
|
+
attr_reader :original_collection
|
22
|
+
|
23
|
+
class << self
|
24
|
+
extend T::Sig
|
25
|
+
|
26
|
+
# Get all packages wrapped in our interfaces
|
27
|
+
sig { returns(QueryPackwerk::Packages) }
|
28
|
+
def all
|
29
|
+
@all ||= T.let(
|
30
|
+
begin
|
31
|
+
packages = ParsePackwerk.all.map { |p| QueryPackwerk::Package.new(original_package: p) }
|
32
|
+
QueryPackwerk::Packages.new(packages)
|
33
|
+
end,
|
34
|
+
T.nilable(QueryPackwerk::Packages)
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
sig do
|
39
|
+
params(
|
40
|
+
query_params: T.untyped, # Array, or anything responding to `===`, which can't be typed
|
41
|
+
query_fn: T.nilable(T.proc.params(arg0: T.untyped).returns(T::Boolean))
|
42
|
+
).returns(QueryPackwerk::Packages)
|
43
|
+
end
|
44
|
+
def where(**query_params, &query_fn)
|
45
|
+
QueryPackwerk::Packages.new(super)
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { void }
|
49
|
+
def reload!
|
50
|
+
@all = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { params(original_collection: T::Array[QueryPackwerk::Package]).void }
|
55
|
+
def initialize(original_collection)
|
56
|
+
@original_collection = original_collection
|
57
|
+
end
|
58
|
+
|
59
|
+
# You can query for packages rather than violations to get a broader view, and
|
60
|
+
# the violations returned from this will be related to all packs in this class rather than just
|
61
|
+
# one.
|
62
|
+
sig { returns(QueryPackwerk::Violations) }
|
63
|
+
def violations
|
64
|
+
QueryPackwerk::Violations.new(
|
65
|
+
@original_collection.flat_map { |pack| pack.violations.original_collection }
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { returns(String) }
|
70
|
+
def inspect
|
71
|
+
[
|
72
|
+
"#<#{self.class.name} [",
|
73
|
+
to_a.map(&:inspect).join("\n"),
|
74
|
+
']>'
|
75
|
+
].join("\n")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module QueryPackwerk
|
5
|
+
# A mixin module providing a flexible query interface for collections.
|
6
|
+
# Implements methods for filtering, comparing, and manipulating collection data
|
7
|
+
# with a consistent API pattern. Extends included classes with class methods
|
8
|
+
# for advanced querying capabilities and integrates with Enumerable for
|
9
|
+
# additional collection functionality.
|
10
|
+
module QueryInterface
|
11
|
+
include Kernel
|
12
|
+
|
13
|
+
extend T::Sig
|
14
|
+
extend T::Generic
|
15
|
+
|
16
|
+
Elem = type_member
|
17
|
+
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
# Iterate over every member of the underlying original collection,
|
21
|
+
# also tie-in for Enumerable methods.
|
22
|
+
#
|
23
|
+
# Returns `T.untyped` because Sorbet complains on more accurate returns
|
24
|
+
# or void.
|
25
|
+
sig do
|
26
|
+
override.params(
|
27
|
+
block: T.nilable(T.proc.params(arg0: Elem).returns(T.untyped))
|
28
|
+
).returns(
|
29
|
+
T.any(T::Enumerator[Elem], T::Array[Elem])
|
30
|
+
)
|
31
|
+
end
|
32
|
+
def each(&block)
|
33
|
+
return enum_for(:each) unless block_given?
|
34
|
+
|
35
|
+
original_collection.each(&block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Should be overridden, the base of most of the queries.
|
39
|
+
#
|
40
|
+
# TODO: Consider refactoring to `abstract` interface
|
41
|
+
sig { overridable.returns(T::Array[T.untyped]) }
|
42
|
+
def original_collection
|
43
|
+
[]
|
44
|
+
end
|
45
|
+
|
46
|
+
# `Enumerable` does not expose these methods.
|
47
|
+
sig { returns(Integer) }
|
48
|
+
def size
|
49
|
+
original_collection.size
|
50
|
+
end
|
51
|
+
|
52
|
+
alias length size
|
53
|
+
alias count size
|
54
|
+
|
55
|
+
# Extend the class with class extensions whenever this module is
|
56
|
+
# included so we get both singleton and instance methods we need for
|
57
|
+
# querying.
|
58
|
+
sig { params(klass: T::Class[T.anything]).void }
|
59
|
+
def self.included(klass)
|
60
|
+
klass.extend(ClassMethods)
|
61
|
+
end
|
62
|
+
|
63
|
+
module ClassMethods
|
64
|
+
include Kernel
|
65
|
+
extend T::Sig
|
66
|
+
|
67
|
+
# *Notes on Sorbet:*
|
68
|
+
#
|
69
|
+
# Allows you to query against the underlying collection. As Sorbet does not
|
70
|
+
# allow for interface typing (i.e. `responds_to?`) we're using `T.untyped` instead.
|
71
|
+
#
|
72
|
+
# Since the implementing class will wrap the return value we also return `T.untyped` as
|
73
|
+
# Sorbet will imply this should be a `T::Array[inheriting_class]` rather than `InheritingClass`.
|
74
|
+
#
|
75
|
+
# *Notes on Interface:*
|
76
|
+
#
|
77
|
+
# This is based on a pattern-matching like interface using `===` and a few other assumptions
|
78
|
+
# about the underlying data structure. For singular values that means you can do this:
|
79
|
+
#
|
80
|
+
# InheritingClass.where(name: /part_of_name/)
|
81
|
+
#
|
82
|
+
# ...as `===` works as a pattern inclusion for classes like `Regexp`, `Range`, class types, and
|
83
|
+
# more.
|
84
|
+
#
|
85
|
+
# There are a few unique cases presumed here as well when dealing with more complex queries:
|
86
|
+
#
|
87
|
+
# * If both the underlying value and query value are arrays we check for intersection
|
88
|
+
# * If the query value is an array we check if the underlying value "matches" and item in it
|
89
|
+
# * If the underlying value is an array we check if the query value matches any part of it
|
90
|
+
# * Otherwise we use `===` as a query
|
91
|
+
#
|
92
|
+
# These interfaces are meant to mimic ActiveRecord, but do take some liberties as we're working
|
93
|
+
# with Ruby objects rather than underlying database structures.
|
94
|
+
sig do
|
95
|
+
params(
|
96
|
+
query_params: T.untyped, # Array, or anything responding to `===`, which can't be typed
|
97
|
+
_query_fn: T.nilable(T.proc.params(arg0: T.untyped).returns(T::Boolean))
|
98
|
+
).returns(T.untyped)
|
99
|
+
end
|
100
|
+
def where(**query_params, &_query_fn)
|
101
|
+
query_keys = query_params.keys
|
102
|
+
all_values = all
|
103
|
+
|
104
|
+
accepted_keys = all_values.first.deconstruct_keys(nil).keys
|
105
|
+
invalid_keys = query_keys - accepted_keys
|
106
|
+
|
107
|
+
raise ArgumentError, "The following keys are invalid for querying: #{invalid_keys.join(', ')}" if invalid_keys.any?
|
108
|
+
|
109
|
+
all_values.select do |value|
|
110
|
+
next yield(value) if block_given?
|
111
|
+
|
112
|
+
object_params = value.deconstruct_keys(query_keys)
|
113
|
+
|
114
|
+
query_params.all? do |param, query_matcher|
|
115
|
+
object_value = object_params[param]
|
116
|
+
|
117
|
+
case [query_matcher, object_value]
|
118
|
+
in [Array, Array]
|
119
|
+
intersects?(query_matcher, object_value)
|
120
|
+
in [Array, _]
|
121
|
+
any_compare?(query_matcher, object_value)
|
122
|
+
in [_, Array]
|
123
|
+
includes?(query_matcher, object_value)
|
124
|
+
else
|
125
|
+
case_equal?(query_matcher, object_value)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
# Similar to the above `original_collection` we want to override this by defining
|
131
|
+
# where the InheritingClass can find all of its raw data.
|
132
|
+
#
|
133
|
+
# Sorbet does not recognize `included(klass)`/`klass.extend(ClassMethods)` when
|
134
|
+
# looking for `override` hooks. May be a bug in Sorbet.
|
135
|
+
sig do
|
136
|
+
returns(T.untyped)
|
137
|
+
end
|
138
|
+
def all
|
139
|
+
[]
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Query Array to value Array is intersection, rather than strict equality or a literal pattern
|
145
|
+
# match. While we could do a `===` approximation of intersection that would make
|
146
|
+
# the code much more complicated.
|
147
|
+
sig { params(query_matcher: T::Array[T.untyped], object_value: T::Array[T.untyped]).returns(T::Boolean) }
|
148
|
+
def intersects?(query_matcher, object_value)
|
149
|
+
query_matcher.intersect?(object_value)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Query Array to Any object value checks if any of the query matchers match that value
|
153
|
+
sig { params(query_matcher: T::Array[T.untyped], object_value: T.untyped).returns(T::Boolean) }
|
154
|
+
def any_compare?(query_matcher, object_value)
|
155
|
+
query_matcher.any? { |matcher| matcher === object_value } # rubocop:disable Style/CaseEquality
|
156
|
+
end
|
157
|
+
|
158
|
+
# Any other type of Query to Array is find if any value matches the condition
|
159
|
+
sig { params(query_matcher: T.untyped, object_value: T::Array[T.untyped]).returns(T::Boolean) }
|
160
|
+
def includes?(query_matcher, object_value)
|
161
|
+
object_value.any? { |v| query_matcher === v } # rubocop:disable Style/CaseEquality, Performance/RedundantEqualityComparisonBlock
|
162
|
+
end
|
163
|
+
|
164
|
+
# Otherwise we use `===` to decide if it's a match, and we use it explicitly as
|
165
|
+
# it's a very powerful query-like DSL and is in the language for a reason.
|
166
|
+
#
|
167
|
+
# We would use `===` for all cases, except that `Array` does not define it, nor
|
168
|
+
# should it as it could be defined in multiple different ways much the same as
|
169
|
+
# `String#each` could mean several things.
|
170
|
+
sig { params(query_matcher: T.untyped, object_value: T.untyped).returns(T::Boolean) }
|
171
|
+
def case_equal?(query_matcher, object_value)
|
172
|
+
query_matcher === object_value # rubocop:disable Style/CaseEquality
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
protected
|
177
|
+
|
178
|
+
sig do
|
179
|
+
params(
|
180
|
+
collection: T::Array[T.untyped],
|
181
|
+
threshold: Integer,
|
182
|
+
_blk: T.proc.params(arg0: T.untyped).returns(T::Array[T.untyped])
|
183
|
+
).returns(T::Hash[String, T::Hash[String, Integer]])
|
184
|
+
end
|
185
|
+
def deep_merge_counts(collection, threshold: 0, &_blk)
|
186
|
+
nested_counts = collection.each_with_object(
|
187
|
+
Hash.new { |h, k| h[k] = Hash.new(0) }
|
188
|
+
) do |violation, new_nested_counts|
|
189
|
+
key, values = yield(violation)
|
190
|
+
|
191
|
+
new_nested_counts[key].merge!(values) { |_k, a, b| a + b }
|
192
|
+
end
|
193
|
+
|
194
|
+
threshold_drop(nested_counts, threshold: threshold)
|
195
|
+
end
|
196
|
+
|
197
|
+
sig do
|
198
|
+
params(
|
199
|
+
collection: T::Array[T.untyped],
|
200
|
+
_blk: T.proc.params(arg0: T.untyped).returns(T::Array[T.untyped])
|
201
|
+
).returns(T::Hash[String, T::Array[T.untyped]])
|
202
|
+
end
|
203
|
+
def deep_merge_groups(collection, &_blk)
|
204
|
+
groups = collection.each_with_object(
|
205
|
+
Hash.new { |h, k| h[k] = [] }
|
206
|
+
) do |violation, new_groups|
|
207
|
+
key, values = yield(violation)
|
208
|
+
new_groups[key].concat(values)
|
209
|
+
end
|
210
|
+
|
211
|
+
if groups.values.first.is_a?(String)
|
212
|
+
groups.transform_values(&:sort)
|
213
|
+
else
|
214
|
+
groups
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
sig do
|
219
|
+
params(
|
220
|
+
collection: T::Array[T.untyped],
|
221
|
+
_blk: T.proc.params(arg0: T.untyped).returns(T::Array[T.untyped])
|
222
|
+
).returns(T::Hash[String, T::Hash[String, T::Array[String]]])
|
223
|
+
end
|
224
|
+
def deep_merge_hash_groups(collection, &_blk)
|
225
|
+
initial_hash = Hash.new do |constants, const_name|
|
226
|
+
constants[const_name] = Hash.new do |sources, source|
|
227
|
+
sources[source] = []
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
merged_collection = collection.each_with_object(initial_hash) do |violation, new_groups|
|
232
|
+
constant_name, sources = yield(violation)
|
233
|
+
sources.each do |source, file_locations|
|
234
|
+
new_groups[constant_name][source].concat(file_locations)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
merged_collection.transform_values do |sources_hash|
|
239
|
+
sources_hash
|
240
|
+
.transform_values(&:sort)
|
241
|
+
.sort_by { |_anonymous_source, file_list| -file_list.size }
|
242
|
+
.to_h
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
sig do
|
247
|
+
params(
|
248
|
+
hash: T::Hash[String, T::Hash[String, Integer]],
|
249
|
+
threshold: Integer
|
250
|
+
).returns(T::Hash[String, T::Hash[String, Integer]])
|
251
|
+
end
|
252
|
+
def threshold_drop(hash, threshold: 0)
|
253
|
+
hash
|
254
|
+
.transform_values { |vs| threshold_filter_sort(vs, threshold: threshold) }
|
255
|
+
.reject { |_k, vs| vs.empty? }
|
256
|
+
end
|
257
|
+
|
258
|
+
sig { params(hash: T::Hash[String, Integer], threshold: Integer).returns(T::Hash[String, Integer]) }
|
259
|
+
def threshold_filter_sort(hash, threshold: 0)
|
260
|
+
hash
|
261
|
+
.select { |_k, v| v >= threshold }
|
262
|
+
.sort_by { |_k, v| -v }
|
263
|
+
.to_h
|
264
|
+
end
|
265
|
+
|
266
|
+
mixes_in_class_methods(ClassMethods)
|
267
|
+
end
|
268
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module QueryPackwerk
|
5
|
+
class RuleRewriter
|
6
|
+
class AnonymizeArgumentsRule < BaseRule
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
# Arguments prefixed with a sigil like `*arg` and `&fn`
|
10
|
+
SIGIL_ARGS = T.let(%i[splat block_pass].freeze, T::Array[Symbol])
|
11
|
+
|
12
|
+
sig { override.params(node: RuboCop::AST::Node).void }
|
13
|
+
def on_send(node)
|
14
|
+
return unless node.arguments?
|
15
|
+
|
16
|
+
node.arguments.reject(&:hash_type?).each do |arg|
|
17
|
+
arg_node = if SIGIL_ARGS.include?(arg.type)
|
18
|
+
arg.children.first
|
19
|
+
else
|
20
|
+
arg
|
21
|
+
end
|
22
|
+
|
23
|
+
# Just in case we get strangely shaped nodes
|
24
|
+
next unless arg_node.respond_to?(:loc)
|
25
|
+
|
26
|
+
@rewriter.replace(arg_node.loc.expression, ANONYMIZED)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module QueryPackwerk
|
5
|
+
class RuleRewriter
|
6
|
+
class AnonymizeKeywordArgumentsRule < BaseRule
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { override.params(node: RuboCop::AST::Node).void }
|
10
|
+
def on_send(node)
|
11
|
+
return unless node.arguments?
|
12
|
+
|
13
|
+
node.arguments.select(&:hash_type?).each do |hash_node|
|
14
|
+
hash_node.children.each do |pair|
|
15
|
+
_keyword_node, value_node = if pair.kwsplat_type?
|
16
|
+
[nil, pair.children.first]
|
17
|
+
else
|
18
|
+
pair.children
|
19
|
+
end
|
20
|
+
|
21
|
+
# Just in case we get strangely shaped nodes
|
22
|
+
next unless value_node.respond_to?(:loc)
|
23
|
+
|
24
|
+
@rewriter.replace(value_node.loc.expression, ANONYMIZED)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|